MDL-75210 mod_quiz: fix sequential quiz access
[moodle.git] / mod / quiz / classes / external.php
blob5d41e694462c1fbaed015001163bf2638b0d9f77
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17 /**
18 * Quiz external API
20 * @package mod_quiz
21 * @category external
22 * @copyright 2016 Juan Leyva <juan@moodle.com>
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24 * @since Moodle 3.1
27 defined('MOODLE_INTERNAL') || die;
29 require_once($CFG->libdir . '/externallib.php');
30 require_once($CFG->dirroot . '/mod/quiz/locallib.php');
32 /**
33 * Quiz external functions
35 * @package mod_quiz
36 * @category external
37 * @copyright 2016 Juan Leyva <juan@moodle.com>
38 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
39 * @since Moodle 3.1
41 class mod_quiz_external extends external_api {
43 /**
44 * Describes the parameters for get_quizzes_by_courses.
46 * @return external_function_parameters
47 * @since Moodle 3.1
49 public static function get_quizzes_by_courses_parameters() {
50 return new external_function_parameters (
51 array(
52 'courseids' => new external_multiple_structure(
53 new external_value(PARAM_INT, 'course id'), 'Array of course ids', VALUE_DEFAULT, array()
59 /**
60 * Returns a list of quizzes in a provided list of courses,
61 * if no list is provided all quizzes that the user can view will be returned.
63 * @param array $courseids Array of course ids
64 * @return array of quizzes details
65 * @since Moodle 3.1
67 public static function get_quizzes_by_courses($courseids = array()) {
68 global $USER;
70 $warnings = array();
71 $returnedquizzes = array();
73 $params = array(
74 'courseids' => $courseids,
76 $params = self::validate_parameters(self::get_quizzes_by_courses_parameters(), $params);
78 $mycourses = array();
79 if (empty($params['courseids'])) {
80 $mycourses = enrol_get_my_courses();
81 $params['courseids'] = array_keys($mycourses);
84 // Ensure there are courseids to loop through.
85 if (!empty($params['courseids'])) {
87 list($courses, $warnings) = external_util::validate_courses($params['courseids'], $mycourses);
89 // Get the quizzes in this course, this function checks users visibility permissions.
90 // We can avoid then additional validate_context calls.
91 $quizzes = get_all_instances_in_courses("quiz", $courses);
92 foreach ($quizzes as $quiz) {
93 $context = context_module::instance($quiz->coursemodule);
95 // Update quiz with override information.
96 $quiz = quiz_update_effective_access($quiz, $USER->id);
98 // Entry to return.
99 $quizdetails = array();
100 // First, we return information that any user can see in the web interface.
101 $quizdetails['id'] = $quiz->id;
102 $quizdetails['coursemodule'] = $quiz->coursemodule;
103 $quizdetails['course'] = $quiz->course;
104 $quizdetails['name'] = external_format_string($quiz->name, $context->id);
106 if (has_capability('mod/quiz:view', $context)) {
107 // Format intro.
108 $options = array('noclean' => true);
109 list($quizdetails['intro'], $quizdetails['introformat']) =
110 external_format_text($quiz->intro, $quiz->introformat, $context->id, 'mod_quiz', 'intro', null, $options);
112 $quizdetails['introfiles'] = external_util::get_area_files($context->id, 'mod_quiz', 'intro', false, false);
113 $viewablefields = array('timeopen', 'timeclose', 'grademethod', 'section', 'visible', 'groupmode',
114 'groupingid', 'attempts', 'timelimit', 'grademethod', 'decimalpoints',
115 'questiondecimalpoints', 'sumgrades', 'grade', 'preferredbehaviour');
116 // Some times this function returns just empty.
117 $hasfeedback = quiz_has_feedback($quiz);
118 $quizdetails['hasfeedback'] = (!empty($hasfeedback)) ? 1 : 0;
120 $timenow = time();
121 $quizobj = quiz::create($quiz->id, $USER->id);
122 $accessmanager = new quiz_access_manager($quizobj, $timenow, has_capability('mod/quiz:ignoretimelimits',
123 $context, null, false));
125 // Fields the user could see if have access to the quiz.
126 if (!$accessmanager->prevent_access()) {
127 $quizdetails['hasquestions'] = (int) $quizobj->has_questions();
128 $quizdetails['autosaveperiod'] = get_config('quiz', 'autosaveperiod');
130 $additionalfields = array('attemptonlast', 'reviewattempt', 'reviewcorrectness', 'reviewmarks',
131 'reviewspecificfeedback', 'reviewgeneralfeedback', 'reviewrightanswer',
132 'reviewoverallfeedback', 'questionsperpage', 'navmethod',
133 'browsersecurity', 'delay1', 'delay2', 'showuserpicture', 'showblocks',
134 'completionattemptsexhausted', 'overduehandling',
135 'graceperiod', 'canredoquestions', 'allowofflineattempts');
136 $viewablefields = array_merge($viewablefields, $additionalfields);
138 // Any course module fields that previously existed in quiz.
139 $quizdetails['completionpass'] = $quizobj->get_cm()->completionpassgrade;
142 // Fields only for managers.
143 if (has_capability('moodle/course:manageactivities', $context)) {
144 $additionalfields = array('shuffleanswers', 'timecreated', 'timemodified', 'password', 'subnet');
145 $viewablefields = array_merge($viewablefields, $additionalfields);
148 foreach ($viewablefields as $field) {
149 $quizdetails[$field] = $quiz->{$field};
152 $returnedquizzes[] = $quizdetails;
155 $result = array();
156 $result['quizzes'] = $returnedquizzes;
157 $result['warnings'] = $warnings;
158 return $result;
162 * Describes the get_quizzes_by_courses return value.
164 * @return external_single_structure
165 * @since Moodle 3.1
167 public static function get_quizzes_by_courses_returns() {
168 return new external_single_structure(
169 array(
170 'quizzes' => new external_multiple_structure(
171 new external_single_structure(
172 array(
173 'id' => new external_value(PARAM_INT, 'Standard Moodle primary key.'),
174 'course' => new external_value(PARAM_INT, 'Foreign key reference to the course this quiz is part of.'),
175 'coursemodule' => new external_value(PARAM_INT, 'Course module id.'),
176 'name' => new external_value(PARAM_RAW, 'Quiz name.'),
177 'intro' => new external_value(PARAM_RAW, 'Quiz introduction text.', VALUE_OPTIONAL),
178 'introformat' => new external_format_value('intro', VALUE_OPTIONAL),
179 'introfiles' => new external_files('Files in the introduction text', VALUE_OPTIONAL),
180 'timeopen' => new external_value(PARAM_INT, 'The time when this quiz opens. (0 = no restriction.)',
181 VALUE_OPTIONAL),
182 'timeclose' => new external_value(PARAM_INT, 'The time when this quiz closes. (0 = no restriction.)',
183 VALUE_OPTIONAL),
184 'timelimit' => new external_value(PARAM_INT, 'The time limit for quiz attempts, in seconds.',
185 VALUE_OPTIONAL),
186 'overduehandling' => new external_value(PARAM_ALPHA, 'The method used to handle overdue attempts.
187 \'autosubmit\', \'graceperiod\' or \'autoabandon\'.',
188 VALUE_OPTIONAL),
189 'graceperiod' => new external_value(PARAM_INT, 'The amount of time (in seconds) after the time limit
190 runs out during which attempts can still be submitted,
191 if overduehandling is set to allow it.', VALUE_OPTIONAL),
192 'preferredbehaviour' => new external_value(PARAM_ALPHANUMEXT, 'The behaviour to ask questions to use.',
193 VALUE_OPTIONAL),
194 'canredoquestions' => new external_value(PARAM_INT, 'Allows students to redo any completed question
195 within a quiz attempt.', VALUE_OPTIONAL),
196 'attempts' => new external_value(PARAM_INT, 'The maximum number of attempts a student is allowed.',
197 VALUE_OPTIONAL),
198 'attemptonlast' => new external_value(PARAM_INT, 'Whether subsequent attempts start from the answer
199 to the previous attempt (1) or start blank (0).',
200 VALUE_OPTIONAL),
201 'grademethod' => new external_value(PARAM_INT, 'One of the values QUIZ_GRADEHIGHEST, QUIZ_GRADEAVERAGE,
202 QUIZ_ATTEMPTFIRST or QUIZ_ATTEMPTLAST.', VALUE_OPTIONAL),
203 'decimalpoints' => new external_value(PARAM_INT, 'Number of decimal points to use when displaying
204 grades.', VALUE_OPTIONAL),
205 'questiondecimalpoints' => new external_value(PARAM_INT, 'Number of decimal points to use when
206 displaying question grades.
207 (-1 means use decimalpoints.)', VALUE_OPTIONAL),
208 'reviewattempt' => new external_value(PARAM_INT, 'Whether users are allowed to review their quiz
209 attempts at various times. This is a bit field, decoded by the
210 mod_quiz_display_options class. It is formed by ORing together
211 the constants defined there.', VALUE_OPTIONAL),
212 'reviewcorrectness' => new external_value(PARAM_INT, 'Whether users are allowed to review their quiz
213 attempts at various times.
214 A bit field, like reviewattempt.', VALUE_OPTIONAL),
215 'reviewmarks' => new external_value(PARAM_INT, 'Whether users are allowed to review their quiz attempts
216 at various times. A bit field, like reviewattempt.',
217 VALUE_OPTIONAL),
218 'reviewspecificfeedback' => new external_value(PARAM_INT, 'Whether users are allowed to review their
219 quiz attempts at various times. A bit field, like
220 reviewattempt.', VALUE_OPTIONAL),
221 'reviewgeneralfeedback' => new external_value(PARAM_INT, 'Whether users are allowed to review their
222 quiz attempts at various times. A bit field, like
223 reviewattempt.', VALUE_OPTIONAL),
224 'reviewrightanswer' => new external_value(PARAM_INT, 'Whether users are allowed to review their quiz
225 attempts at various times. A bit field, like
226 reviewattempt.', VALUE_OPTIONAL),
227 'reviewoverallfeedback' => new external_value(PARAM_INT, 'Whether users are allowed to review their quiz
228 attempts at various times. A bit field, like
229 reviewattempt.', VALUE_OPTIONAL),
230 'questionsperpage' => new external_value(PARAM_INT, 'How often to insert a page break when editing
231 the quiz, or when shuffling the question order.',
232 VALUE_OPTIONAL),
233 'navmethod' => new external_value(PARAM_ALPHA, 'Any constraints on how the user is allowed to navigate
234 around the quiz. Currently recognised values are
235 \'free\' and \'seq\'.', VALUE_OPTIONAL),
236 'shuffleanswers' => new external_value(PARAM_INT, 'Whether the parts of the question should be shuffled,
237 in those question types that support it.', VALUE_OPTIONAL),
238 'sumgrades' => new external_value(PARAM_FLOAT, 'The total of all the question instance maxmarks.',
239 VALUE_OPTIONAL),
240 'grade' => new external_value(PARAM_FLOAT, 'The total that the quiz overall grade is scaled to be
241 out of.', VALUE_OPTIONAL),
242 'timecreated' => new external_value(PARAM_INT, 'The time when the quiz was added to the course.',
243 VALUE_OPTIONAL),
244 'timemodified' => new external_value(PARAM_INT, 'Last modified time.',
245 VALUE_OPTIONAL),
246 'password' => new external_value(PARAM_RAW, 'A password that the student must enter before starting or
247 continuing a quiz attempt.', VALUE_OPTIONAL),
248 'subnet' => new external_value(PARAM_RAW, 'Used to restrict the IP addresses from which this quiz can
249 be attempted. The format is as requried by the address_in_subnet
250 function.', VALUE_OPTIONAL),
251 'browsersecurity' => new external_value(PARAM_ALPHANUMEXT, 'Restriciton on the browser the student must
252 use. E.g. \'securewindow\'.', VALUE_OPTIONAL),
253 'delay1' => new external_value(PARAM_INT, 'Delay that must be left between the first and second attempt,
254 in seconds.', VALUE_OPTIONAL),
255 'delay2' => new external_value(PARAM_INT, 'Delay that must be left between the second and subsequent
256 attempt, in seconds.', VALUE_OPTIONAL),
257 'showuserpicture' => new external_value(PARAM_INT, 'Option to show the user\'s picture during the
258 attempt and on the review page.', VALUE_OPTIONAL),
259 'showblocks' => new external_value(PARAM_INT, 'Whether blocks should be shown on the attempt.php and
260 review.php pages.', VALUE_OPTIONAL),
261 'completionattemptsexhausted' => new external_value(PARAM_INT, 'Mark quiz complete when the student has
262 exhausted the maximum number of attempts',
263 VALUE_OPTIONAL),
264 'completionpass' => new external_value(PARAM_INT, 'Whether to require passing grade', VALUE_OPTIONAL),
265 'allowofflineattempts' => new external_value(PARAM_INT, 'Whether to allow the quiz to be attempted
266 offline in the mobile app', VALUE_OPTIONAL),
267 'autosaveperiod' => new external_value(PARAM_INT, 'Auto-save delay', VALUE_OPTIONAL),
268 'hasfeedback' => new external_value(PARAM_INT, 'Whether the quiz has any non-blank feedback text',
269 VALUE_OPTIONAL),
270 'hasquestions' => new external_value(PARAM_INT, 'Whether the quiz has questions', VALUE_OPTIONAL),
271 'section' => new external_value(PARAM_INT, 'Course section id', VALUE_OPTIONAL),
272 'visible' => new external_value(PARAM_INT, 'Module visibility', VALUE_OPTIONAL),
273 'groupmode' => new external_value(PARAM_INT, 'Group mode', VALUE_OPTIONAL),
274 'groupingid' => new external_value(PARAM_INT, 'Grouping id', VALUE_OPTIONAL),
278 'warnings' => new external_warnings(),
285 * Utility function for validating a quiz.
287 * @param int $quizid quiz instance id
288 * @return array array containing the quiz, course, context and course module objects
289 * @since Moodle 3.1
291 protected static function validate_quiz($quizid) {
292 global $DB;
294 // Request and permission validation.
295 $quiz = $DB->get_record('quiz', array('id' => $quizid), '*', MUST_EXIST);
296 list($course, $cm) = get_course_and_cm_from_instance($quiz, 'quiz');
298 $context = context_module::instance($cm->id);
299 self::validate_context($context);
301 return array($quiz, $course, $cm, $context);
305 * Describes the parameters for view_quiz.
307 * @return external_function_parameters
308 * @since Moodle 3.1
310 public static function view_quiz_parameters() {
311 return new external_function_parameters (
312 array(
313 'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
319 * Trigger the course module viewed event and update the module completion status.
321 * @param int $quizid quiz instance id
322 * @return array of warnings and status result
323 * @since Moodle 3.1
324 * @throws moodle_exception
326 public static function view_quiz($quizid) {
327 global $DB;
329 $params = self::validate_parameters(self::view_quiz_parameters(), array('quizid' => $quizid));
330 $warnings = array();
332 list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
334 // Trigger course_module_viewed event and completion.
335 quiz_view($quiz, $course, $cm, $context);
337 $result = array();
338 $result['status'] = true;
339 $result['warnings'] = $warnings;
340 return $result;
344 * Describes the view_quiz return value.
346 * @return external_single_structure
347 * @since Moodle 3.1
349 public static function view_quiz_returns() {
350 return new external_single_structure(
351 array(
352 'status' => new external_value(PARAM_BOOL, 'status: true if success'),
353 'warnings' => new external_warnings(),
359 * Describes the parameters for get_user_attempts.
361 * @return external_function_parameters
362 * @since Moodle 3.1
364 public static function get_user_attempts_parameters() {
365 return new external_function_parameters (
366 array(
367 'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
368 'userid' => new external_value(PARAM_INT, 'user id, empty for current user', VALUE_DEFAULT, 0),
369 'status' => new external_value(PARAM_ALPHA, 'quiz status: all, finished or unfinished', VALUE_DEFAULT, 'finished'),
370 'includepreviews' => new external_value(PARAM_BOOL, 'whether to include previews or not', VALUE_DEFAULT, false),
377 * Return a list of attempts for the given quiz and user.
379 * @param int $quizid quiz instance id
380 * @param int $userid user id
381 * @param string $status quiz status: all, finished or unfinished
382 * @param bool $includepreviews whether to include previews or not
383 * @return array of warnings and the list of attempts
384 * @since Moodle 3.1
385 * @throws invalid_parameter_exception
387 public static function get_user_attempts($quizid, $userid = 0, $status = 'finished', $includepreviews = false) {
388 global $DB, $USER;
390 $warnings = array();
392 $params = array(
393 'quizid' => $quizid,
394 'userid' => $userid,
395 'status' => $status,
396 'includepreviews' => $includepreviews,
398 $params = self::validate_parameters(self::get_user_attempts_parameters(), $params);
400 list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
402 if (!in_array($params['status'], array('all', 'finished', 'unfinished'))) {
403 throw new invalid_parameter_exception('Invalid status value');
406 // Default value for userid.
407 if (empty($params['userid'])) {
408 $params['userid'] = $USER->id;
411 $user = core_user::get_user($params['userid'], '*', MUST_EXIST);
412 core_user::require_active_user($user);
414 // Extra checks so only users with permissions can view other users attempts.
415 if ($USER->id != $user->id) {
416 require_capability('mod/quiz:viewreports', $context);
419 // Update quiz with override information.
420 $quiz = quiz_update_effective_access($quiz, $params['userid']);
421 $attempts = quiz_get_user_attempts($quiz->id, $user->id, $params['status'], $params['includepreviews']);
422 $attemptresponse = [];
423 foreach ($attempts as $attempt) {
424 $reviewoptions = quiz_get_review_options($quiz, $attempt, $context);
425 if (!has_capability('mod/quiz:viewreports', $context) &&
426 ($reviewoptions->marks < question_display_options::MARK_AND_MAX || $attempt->state != quiz_attempt::FINISHED)) {
427 // Blank the mark if the teacher does not allow it.
428 $attempt->sumgrades = null;
430 $attemptresponse[] = $attempt;
432 $result = array();
433 $result['attempts'] = $attemptresponse;
434 $result['warnings'] = $warnings;
435 return $result;
439 * Describes a single attempt structure.
441 * @return external_single_structure the attempt structure
443 private static function attempt_structure() {
444 return new external_single_structure(
445 array(
446 'id' => new external_value(PARAM_INT, 'Attempt id.', VALUE_OPTIONAL),
447 'quiz' => new external_value(PARAM_INT, 'Foreign key reference to the quiz that was attempted.',
448 VALUE_OPTIONAL),
449 'userid' => new external_value(PARAM_INT, 'Foreign key reference to the user whose attempt this is.',
450 VALUE_OPTIONAL),
451 'attempt' => new external_value(PARAM_INT, 'Sequentially numbers this students attempts at this quiz.',
452 VALUE_OPTIONAL),
453 'uniqueid' => new external_value(PARAM_INT, 'Foreign key reference to the question_usage that holds the
454 details of the the question_attempts that make up this quiz
455 attempt.', VALUE_OPTIONAL),
456 'layout' => new external_value(PARAM_RAW, 'Attempt layout.', VALUE_OPTIONAL),
457 'currentpage' => new external_value(PARAM_INT, 'Attempt current page.', VALUE_OPTIONAL),
458 'preview' => new external_value(PARAM_INT, 'Whether is a preview attempt or not.', VALUE_OPTIONAL),
459 'state' => new external_value(PARAM_ALPHA, 'The current state of the attempts. \'inprogress\',
460 \'overdue\', \'finished\' or \'abandoned\'.', VALUE_OPTIONAL),
461 'timestart' => new external_value(PARAM_INT, 'Time when the attempt was started.', VALUE_OPTIONAL),
462 'timefinish' => new external_value(PARAM_INT, 'Time when the attempt was submitted.
463 0 if the attempt has not been submitted yet.', VALUE_OPTIONAL),
464 'timemodified' => new external_value(PARAM_INT, 'Last modified time.', VALUE_OPTIONAL),
465 'timemodifiedoffline' => new external_value(PARAM_INT, 'Last modified time via webservices.', VALUE_OPTIONAL),
466 'timecheckstate' => new external_value(PARAM_INT, 'Next time quiz cron should check attempt for
467 state changes. NULL means never check.', VALUE_OPTIONAL),
468 'sumgrades' => new external_value(PARAM_FLOAT, 'Total marks for this attempt.', VALUE_OPTIONAL),
469 'gradednotificationsenttime' => new external_value(PARAM_INT,
470 'Time when the student was notified that manual grading of their attempt was complete.', VALUE_OPTIONAL),
476 * Describes the get_user_attempts return value.
478 * @return external_single_structure
479 * @since Moodle 3.1
481 public static function get_user_attempts_returns() {
482 return new external_single_structure(
483 array(
484 'attempts' => new external_multiple_structure(self::attempt_structure()),
485 'warnings' => new external_warnings(),
491 * Describes the parameters for get_user_best_grade.
493 * @return external_function_parameters
494 * @since Moodle 3.1
496 public static function get_user_best_grade_parameters() {
497 return new external_function_parameters (
498 array(
499 'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
500 'userid' => new external_value(PARAM_INT, 'user id', VALUE_DEFAULT, 0),
506 * Get the best current grade for the given user on a quiz.
508 * @param int $quizid quiz instance id
509 * @param int $userid user id
510 * @return array of warnings and the grade information
511 * @since Moodle 3.1
513 public static function get_user_best_grade($quizid, $userid = 0) {
514 global $DB, $USER, $CFG;
515 require_once($CFG->libdir . '/gradelib.php');
517 $warnings = array();
519 $params = array(
520 'quizid' => $quizid,
521 'userid' => $userid,
523 $params = self::validate_parameters(self::get_user_best_grade_parameters(), $params);
525 list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
527 // Default value for userid.
528 if (empty($params['userid'])) {
529 $params['userid'] = $USER->id;
532 $user = core_user::get_user($params['userid'], '*', MUST_EXIST);
533 core_user::require_active_user($user);
535 // Extra checks so only users with permissions can view other users attempts.
536 if ($USER->id != $user->id) {
537 require_capability('mod/quiz:viewreports', $context);
540 $result = array();
542 // This code was mostly copied from mod/quiz/view.php. We need to make the web service logic consistent.
543 // Get this user's attempts.
544 $attempts = quiz_get_user_attempts($quiz->id, $user->id, 'all');
545 $canviewgrade = false;
546 if ($attempts) {
547 if ($USER->id != $user->id) {
548 // No need to check the permission here. We did it at by require_capability('mod/quiz:viewreports', $context).
549 $canviewgrade = true;
550 } else {
551 // Work out which columns we need, taking account what data is available in each attempt.
552 [$notused, $alloptions] = quiz_get_combined_reviewoptions($quiz, $attempts);
553 $canviewgrade = $alloptions->marks >= question_display_options::MARK_AND_MAX;
557 $grade = $canviewgrade ? quiz_get_best_grade($quiz, $user->id) : null;
559 if ($grade === null) {
560 $result['hasgrade'] = false;
561 } else {
562 $result['hasgrade'] = true;
563 $result['grade'] = $grade;
566 // Inform user of the grade to pass if non-zero.
567 $gradinginfo = grade_get_grades($course->id, 'mod', 'quiz', $quiz->id, $user->id);
568 if (!empty($gradinginfo->items)) {
569 $item = $gradinginfo->items[0];
571 if ($item && grade_floats_different($item->gradepass, 0)) {
572 $result['gradetopass'] = $item->gradepass;
576 $result['warnings'] = $warnings;
577 return $result;
581 * Describes the get_user_best_grade return value.
583 * @return external_single_structure
584 * @since Moodle 3.1
586 public static function get_user_best_grade_returns() {
587 return new external_single_structure(
588 array(
589 'hasgrade' => new external_value(PARAM_BOOL, 'Whether the user has a grade on the given quiz.'),
590 'grade' => new external_value(PARAM_FLOAT, 'The grade (only if the user has a grade).', VALUE_OPTIONAL),
591 'gradetopass' => new external_value(PARAM_FLOAT, 'The grade to pass the quiz (only if set).', VALUE_OPTIONAL),
592 'warnings' => new external_warnings(),
598 * Describes the parameters for get_combined_review_options.
600 * @return external_function_parameters
601 * @since Moodle 3.1
603 public static function get_combined_review_options_parameters() {
604 return new external_function_parameters (
605 array(
606 'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
607 'userid' => new external_value(PARAM_INT, 'user id (empty for current user)', VALUE_DEFAULT, 0),
614 * Combines the review options from a number of different quiz attempts.
616 * @param int $quizid quiz instance id
617 * @param int $userid user id (empty for current user)
618 * @return array of warnings and the review options
619 * @since Moodle 3.1
621 public static function get_combined_review_options($quizid, $userid = 0) {
622 global $DB, $USER;
624 $warnings = array();
626 $params = array(
627 'quizid' => $quizid,
628 'userid' => $userid,
630 $params = self::validate_parameters(self::get_combined_review_options_parameters(), $params);
632 list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
634 // Default value for userid.
635 if (empty($params['userid'])) {
636 $params['userid'] = $USER->id;
639 $user = core_user::get_user($params['userid'], '*', MUST_EXIST);
640 core_user::require_active_user($user);
642 // Extra checks so only users with permissions can view other users attempts.
643 if ($USER->id != $user->id) {
644 require_capability('mod/quiz:viewreports', $context);
647 $attempts = quiz_get_user_attempts($quiz->id, $user->id, 'all', true);
649 $result = array();
650 $result['someoptions'] = [];
651 $result['alloptions'] = [];
653 list($someoptions, $alloptions) = quiz_get_combined_reviewoptions($quiz, $attempts);
655 foreach (array('someoptions', 'alloptions') as $typeofoption) {
656 foreach ($$typeofoption as $key => $value) {
657 $result[$typeofoption][] = array(
658 "name" => $key,
659 "value" => (!empty($value)) ? $value : 0
664 $result['warnings'] = $warnings;
665 return $result;
669 * Describes the get_combined_review_options return value.
671 * @return external_single_structure
672 * @since Moodle 3.1
674 public static function get_combined_review_options_returns() {
675 return new external_single_structure(
676 array(
677 'someoptions' => new external_multiple_structure(
678 new external_single_structure(
679 array(
680 'name' => new external_value(PARAM_ALPHANUMEXT, 'option name'),
681 'value' => new external_value(PARAM_INT, 'option value'),
685 'alloptions' => new external_multiple_structure(
686 new external_single_structure(
687 array(
688 'name' => new external_value(PARAM_ALPHANUMEXT, 'option name'),
689 'value' => new external_value(PARAM_INT, 'option value'),
693 'warnings' => new external_warnings(),
699 * Describes the parameters for start_attempt.
701 * @return external_function_parameters
702 * @since Moodle 3.1
704 public static function start_attempt_parameters() {
705 return new external_function_parameters (
706 array(
707 'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
708 'preflightdata' => new external_multiple_structure(
709 new external_single_structure(
710 array(
711 'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
712 'value' => new external_value(PARAM_RAW, 'data value'),
714 ), 'Preflight required data (like passwords)', VALUE_DEFAULT, array()
716 'forcenew' => new external_value(PARAM_BOOL, 'Whether to force a new attempt or not.', VALUE_DEFAULT, false),
723 * Starts a new attempt at a quiz.
725 * @param int $quizid quiz instance id
726 * @param array $preflightdata preflight required data (like passwords)
727 * @param bool $forcenew Whether to force a new attempt or not.
728 * @return array of warnings and the attempt basic data
729 * @since Moodle 3.1
730 * @throws moodle_quiz_exception
732 public static function start_attempt($quizid, $preflightdata = array(), $forcenew = false) {
733 global $DB, $USER;
735 $warnings = array();
736 $attempt = array();
738 $params = array(
739 'quizid' => $quizid,
740 'preflightdata' => $preflightdata,
741 'forcenew' => $forcenew,
743 $params = self::validate_parameters(self::start_attempt_parameters(), $params);
744 $forcenew = $params['forcenew'];
746 list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
748 $quizobj = quiz::create($cm->instance, $USER->id);
750 // Check questions.
751 if (!$quizobj->has_questions()) {
752 throw new moodle_quiz_exception($quizobj, 'noquestionsfound');
755 // Create an object to manage all the other (non-roles) access rules.
756 $timenow = time();
757 $accessmanager = $quizobj->get_access_manager($timenow);
759 // Validate permissions for creating a new attempt and start a new preview attempt if required.
760 list($currentattemptid, $attemptnumber, $lastattempt, $messages, $page) =
761 quiz_validate_new_attempt($quizobj, $accessmanager, $forcenew, -1, false);
763 // Check access.
764 if (!$quizobj->is_preview_user() && $messages) {
765 // Create warnings with the exact messages.
766 foreach ($messages as $message) {
767 $warnings[] = array(
768 'item' => 'quiz',
769 'itemid' => $quiz->id,
770 'warningcode' => '1',
771 'message' => clean_text($message, PARAM_TEXT)
774 } else {
775 if ($accessmanager->is_preflight_check_required($currentattemptid)) {
776 // Need to do some checks before allowing the user to continue.
778 $provideddata = array();
779 foreach ($params['preflightdata'] as $data) {
780 $provideddata[$data['name']] = $data['value'];
783 $errors = $accessmanager->validate_preflight_check($provideddata, [], $currentattemptid);
785 if (!empty($errors)) {
786 throw new moodle_quiz_exception($quizobj, array_shift($errors));
789 // Pre-flight check passed.
790 $accessmanager->notify_preflight_check_passed($currentattemptid);
793 if ($currentattemptid) {
794 if ($lastattempt->state == quiz_attempt::OVERDUE) {
795 throw new moodle_quiz_exception($quizobj, 'stateoverdue');
796 } else {
797 throw new moodle_quiz_exception($quizobj, 'attemptstillinprogress');
800 $offlineattempt = WS_SERVER ? true : false;
801 $attempt = quiz_prepare_and_start_new_attempt($quizobj, $attemptnumber, $lastattempt, $offlineattempt);
804 $result = array();
805 $result['attempt'] = $attempt;
806 $result['warnings'] = $warnings;
807 return $result;
811 * Describes the start_attempt return value.
813 * @return external_single_structure
814 * @since Moodle 3.1
816 public static function start_attempt_returns() {
817 return new external_single_structure(
818 array(
819 'attempt' => self::attempt_structure(),
820 'warnings' => new external_warnings(),
826 * Utility function for validating a given attempt
828 * @param array $params array of parameters including the attemptid and preflight data
829 * @param bool $checkaccessrules whether to check the quiz access rules or not
830 * @param bool $failifoverdue whether to return error if the attempt is overdue
831 * @return array containing the attempt object and access messages
832 * @throws moodle_quiz_exception
833 * @since Moodle 3.1
835 protected static function validate_attempt($params, $checkaccessrules = true, $failifoverdue = true) {
836 global $USER;
838 $attemptobj = quiz_attempt::create($params['attemptid']);
840 $context = context_module::instance($attemptobj->get_cm()->id);
841 self::validate_context($context);
843 // Check that this attempt belongs to this user.
844 if ($attemptobj->get_userid() != $USER->id) {
845 throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'notyourattempt');
848 // General capabilities check.
849 $ispreviewuser = $attemptobj->is_preview_user();
850 if (!$ispreviewuser) {
851 $attemptobj->require_capability('mod/quiz:attempt');
854 // Check the access rules.
855 $accessmanager = $attemptobj->get_access_manager(time());
856 $messages = array();
857 if ($checkaccessrules) {
858 // If the attempt is now overdue, or abandoned, deal with that.
859 $attemptobj->handle_if_time_expired(time(), true);
861 $messages = $accessmanager->prevent_access();
862 if (!$ispreviewuser && $messages) {
863 throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'attempterror');
867 // Attempt closed?.
868 if ($attemptobj->is_finished()) {
869 throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'attemptalreadyclosed');
870 } else if ($failifoverdue && $attemptobj->get_state() == quiz_attempt::OVERDUE) {
871 throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'stateoverdue');
874 // User submitted data (like the quiz password).
875 if ($accessmanager->is_preflight_check_required($attemptobj->get_attemptid())) {
876 $provideddata = array();
877 foreach ($params['preflightdata'] as $data) {
878 $provideddata[$data['name']] = $data['value'];
881 $errors = $accessmanager->validate_preflight_check($provideddata, [], $params['attemptid']);
882 if (!empty($errors)) {
883 throw new moodle_quiz_exception($attemptobj->get_quizobj(), array_shift($errors));
885 // Pre-flight check passed.
886 $accessmanager->notify_preflight_check_passed($params['attemptid']);
889 if (isset($params['page'])) {
890 // Check if the page is out of range.
891 if ($params['page'] != $attemptobj->force_page_number_into_range($params['page'])) {
892 throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'Invalid page number');
895 // Prevent out of sequence access.
896 if (!$attemptobj->check_page_access($params['page'])) {
897 throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'Out of sequence access');
900 // Check slots.
901 $slots = $attemptobj->get_slots($params['page']);
903 if (empty($slots)) {
904 throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'noquestionsfound');
908 return array($attemptobj, $messages);
912 * Describes a single question structure.
914 * @return external_single_structure the question data. Some fields may not be returned depending on the quiz display settings.
915 * @since Moodle 3.1
916 * @since Moodle 3.2 blockedbyprevious parameter added.
918 private static function question_structure() {
919 return new external_single_structure(
920 array(
921 'slot' => new external_value(PARAM_INT, 'slot number'),
922 'type' => new external_value(PARAM_ALPHANUMEXT, 'question type, i.e: multichoice'),
923 'page' => new external_value(PARAM_INT, 'page of the quiz this question appears on'),
924 'html' => new external_value(PARAM_RAW, 'the question rendered'),
925 'responsefileareas' => new external_multiple_structure(
926 new external_single_structure(
927 array(
928 'area' => new external_value(PARAM_NOTAGS, 'File area name'),
929 'files' => new external_files('Response files for the question', VALUE_OPTIONAL),
931 ), 'Response file areas including files', VALUE_OPTIONAL
933 'sequencecheck' => new external_value(PARAM_INT, 'the number of real steps in this attempt', VALUE_OPTIONAL),
934 'lastactiontime' => new external_value(PARAM_INT, 'the timestamp of the most recent step in this question attempt',
935 VALUE_OPTIONAL),
936 'hasautosavedstep' => new external_value(PARAM_BOOL, 'whether this question attempt has autosaved data',
937 VALUE_OPTIONAL),
938 'flagged' => new external_value(PARAM_BOOL, 'whether the question is flagged or not'),
939 'number' => new external_value(PARAM_INT, 'question ordering number in the quiz', VALUE_OPTIONAL),
940 'state' => new external_value(PARAM_ALPHA, 'the state where the question is in.
941 It will not be returned if the user cannot see it due to the quiz display correctness settings.',
942 VALUE_OPTIONAL),
943 'status' => new external_value(PARAM_RAW, 'current formatted state of the question', VALUE_OPTIONAL),
944 'blockedbyprevious' => new external_value(PARAM_BOOL, 'whether the question is blocked by the previous question',
945 VALUE_OPTIONAL),
946 'mark' => new external_value(PARAM_RAW, 'the mark awarded.
947 It will be returned only if the user is allowed to see it.', VALUE_OPTIONAL),
948 'maxmark' => new external_value(PARAM_FLOAT, 'the maximum mark possible for this question attempt.
949 It will be returned only if the user is allowed to see it.', VALUE_OPTIONAL),
950 'settings' => new external_value(PARAM_RAW, 'Question settings (JSON encoded).', VALUE_OPTIONAL),
952 'The question data. Some fields may not be returned depending on the quiz display settings.'
957 * Return questions information for a given attempt.
959 * @param quiz_attempt $attemptobj the quiz attempt object
960 * @param bool $review whether if we are in review mode or not
961 * @param mixed $page string 'all' or integer page number
962 * @return array array of questions including data
964 private static function get_attempt_questions_data(quiz_attempt $attemptobj, $review, $page = 'all') {
965 global $PAGE;
967 $questions = array();
968 $contextid = $attemptobj->get_quizobj()->get_context()->id;
969 $displayoptions = $attemptobj->get_display_options($review);
970 $renderer = $PAGE->get_renderer('mod_quiz');
971 $contextid = $attemptobj->get_quizobj()->get_context()->id;
973 foreach ($attemptobj->get_slots($page) as $slot) {
974 $qtype = $attemptobj->get_question_type_name($slot);
975 $qattempt = $attemptobj->get_question_attempt($slot);
976 $questiondef = $qattempt->get_question(true);
978 // Get response files (for questions like essay that allows attachments).
979 $responsefileareas = [];
980 foreach (question_bank::get_qtype($qtype)->response_file_areas() as $area) {
981 if ($files = $attemptobj->get_question_attempt($slot)->get_last_qt_files($area, $contextid)) {
982 $responsefileareas[$area]['area'] = $area;
983 $responsefileareas[$area]['files'] = [];
985 foreach ($files as $file) {
986 $responsefileareas[$area]['files'][] = array(
987 'filename' => $file->get_filename(),
988 'fileurl' => $qattempt->get_response_file_url($file),
989 'filesize' => $file->get_filesize(),
990 'filepath' => $file->get_filepath(),
991 'mimetype' => $file->get_mimetype(),
992 'timemodified' => $file->get_timemodified(),
998 // Check display settings for question.
999 $settings = $questiondef->get_question_definition_for_external_rendering($qattempt, $displayoptions);
1001 $question = array(
1002 'slot' => $slot,
1003 'type' => $qtype,
1004 'page' => $attemptobj->get_question_page($slot),
1005 'flagged' => $attemptobj->is_question_flagged($slot),
1006 'html' => $attemptobj->render_question($slot, $review, $renderer) . $PAGE->requires->get_end_code(),
1007 'responsefileareas' => $responsefileareas,
1008 'sequencecheck' => $qattempt->get_sequence_check_count(),
1009 'lastactiontime' => $qattempt->get_last_step()->get_timecreated(),
1010 'hasautosavedstep' => $qattempt->has_autosaved_step(),
1011 'settings' => !empty($settings) ? json_encode($settings) : null,
1014 if ($attemptobj->is_real_question($slot)) {
1015 $question['number'] = $attemptobj->get_question_number($slot);
1016 $showcorrectness = $displayoptions->correctness && $qattempt->has_marks();
1017 if ($showcorrectness) {
1018 $question['state'] = (string) $attemptobj->get_question_state($slot);
1020 $question['status'] = $attemptobj->get_question_status($slot, $displayoptions->correctness);
1021 $question['blockedbyprevious'] = $attemptobj->is_blocked_by_previous_question($slot);
1023 if ($displayoptions->marks >= question_display_options::MAX_ONLY) {
1024 $question['maxmark'] = $qattempt->get_max_mark();
1026 if ($displayoptions->marks >= question_display_options::MARK_AND_MAX) {
1027 $question['mark'] = $attemptobj->get_question_mark($slot);
1029 if ($attemptobj->check_page_access($attemptobj->get_question_page($slot), false)) {
1030 $questions[] = $question;
1033 return $questions;
1037 * Describes the parameters for get_attempt_data.
1039 * @return external_function_parameters
1040 * @since Moodle 3.1
1042 public static function get_attempt_data_parameters() {
1043 return new external_function_parameters (
1044 array(
1045 'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1046 'page' => new external_value(PARAM_INT, 'page number'),
1047 'preflightdata' => new external_multiple_structure(
1048 new external_single_structure(
1049 array(
1050 'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
1051 'value' => new external_value(PARAM_RAW, 'data value'),
1053 ), 'Preflight required data (like passwords)', VALUE_DEFAULT, array()
1060 * Returns information for the given attempt page for a quiz attempt in progress.
1062 * @param int $attemptid attempt id
1063 * @param int $page page number
1064 * @param array $preflightdata preflight required data (like passwords)
1065 * @return array of warnings and the attempt data, next page, message and questions
1066 * @since Moodle 3.1
1067 * @throws moodle_quiz_exceptions
1069 public static function get_attempt_data($attemptid, $page, $preflightdata = array()) {
1071 $warnings = array();
1073 $params = array(
1074 'attemptid' => $attemptid,
1075 'page' => $page,
1076 'preflightdata' => $preflightdata,
1078 $params = self::validate_parameters(self::get_attempt_data_parameters(), $params);
1080 list($attemptobj, $messages) = self::validate_attempt($params);
1082 if ($attemptobj->is_last_page($params['page'])) {
1083 $nextpage = -1;
1084 } else {
1085 $nextpage = $params['page'] + 1;
1088 $result = array();
1089 $result['attempt'] = $attemptobj->get_attempt();
1090 $result['messages'] = $messages;
1091 $result['nextpage'] = $nextpage;
1092 $result['warnings'] = $warnings;
1093 $result['questions'] = self::get_attempt_questions_data($attemptobj, false, $params['page']);
1095 return $result;
1099 * Describes the get_attempt_data return value.
1101 * @return external_single_structure
1102 * @since Moodle 3.1
1104 public static function get_attempt_data_returns() {
1105 return new external_single_structure(
1106 array(
1107 'attempt' => self::attempt_structure(),
1108 'messages' => new external_multiple_structure(
1109 new external_value(PARAM_TEXT, 'access message'),
1110 'access messages, will only be returned for users with mod/quiz:preview capability,
1111 for other users this method will throw an exception if there are messages'),
1112 'nextpage' => new external_value(PARAM_INT, 'next page number'),
1113 'questions' => new external_multiple_structure(self::question_structure()),
1114 'warnings' => new external_warnings(),
1120 * Describes the parameters for get_attempt_summary.
1122 * @return external_function_parameters
1123 * @since Moodle 3.1
1125 public static function get_attempt_summary_parameters() {
1126 return new external_function_parameters (
1127 array(
1128 'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1129 'preflightdata' => new external_multiple_structure(
1130 new external_single_structure(
1131 array(
1132 'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
1133 'value' => new external_value(PARAM_RAW, 'data value'),
1135 ), 'Preflight required data (like passwords)', VALUE_DEFAULT, array()
1142 * Returns a summary of a quiz attempt before it is submitted.
1144 * @param int $attemptid attempt id
1145 * @param int $preflightdata preflight required data (like passwords)
1146 * @return array of warnings and the attempt summary data for each question
1147 * @since Moodle 3.1
1149 public static function get_attempt_summary($attemptid, $preflightdata = array()) {
1151 $warnings = array();
1153 $params = array(
1154 'attemptid' => $attemptid,
1155 'preflightdata' => $preflightdata,
1157 $params = self::validate_parameters(self::get_attempt_summary_parameters(), $params);
1159 list($attemptobj, $messages) = self::validate_attempt($params, true, false);
1161 $result = array();
1162 $result['warnings'] = $warnings;
1163 $result['questions'] = self::get_attempt_questions_data($attemptobj, false, 'all');
1165 return $result;
1169 * Describes the get_attempt_summary return value.
1171 * @return external_single_structure
1172 * @since Moodle 3.1
1174 public static function get_attempt_summary_returns() {
1175 return new external_single_structure(
1176 array(
1177 'questions' => new external_multiple_structure(self::question_structure()),
1178 'warnings' => new external_warnings(),
1184 * Describes the parameters for save_attempt.
1186 * @return external_function_parameters
1187 * @since Moodle 3.1
1189 public static function save_attempt_parameters() {
1190 return new external_function_parameters (
1191 array(
1192 'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1193 'data' => new external_multiple_structure(
1194 new external_single_structure(
1195 array(
1196 'name' => new external_value(PARAM_RAW, 'data name'),
1197 'value' => new external_value(PARAM_RAW, 'data value'),
1199 ), 'the data to be saved'
1201 'preflightdata' => new external_multiple_structure(
1202 new external_single_structure(
1203 array(
1204 'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
1205 'value' => new external_value(PARAM_RAW, 'data value'),
1207 ), 'Preflight required data (like passwords)', VALUE_DEFAULT, array()
1214 * Processes save requests during the quiz. This function is intended for the quiz auto-save feature.
1216 * @param int $attemptid attempt id
1217 * @param array $data the data to be saved
1218 * @param array $preflightdata preflight required data (like passwords)
1219 * @return array of warnings and execution result
1220 * @since Moodle 3.1
1222 public static function save_attempt($attemptid, $data, $preflightdata = array()) {
1223 global $DB, $USER;
1225 $warnings = array();
1227 $params = array(
1228 'attemptid' => $attemptid,
1229 'data' => $data,
1230 'preflightdata' => $preflightdata,
1232 $params = self::validate_parameters(self::save_attempt_parameters(), $params);
1234 // Add a page, required by validate_attempt.
1235 list($attemptobj, $messages) = self::validate_attempt($params);
1237 // Prevent functions like file_get_submitted_draft_itemid() or form library requiring a sesskey for WS requests.
1238 if (WS_SERVER || PHPUNIT_TEST) {
1239 $USER->ignoresesskey = true;
1241 $transaction = $DB->start_delegated_transaction();
1242 // Create the $_POST object required by the question engine.
1243 $_POST = array();
1244 foreach ($data as $element) {
1245 $_POST[$element['name']] = $element['value'];
1246 // Some deep core functions like file_get_submitted_draft_itemid() also requires $_REQUEST to be filled.
1247 $_REQUEST[$element['name']] = $element['value'];
1249 $timenow = time();
1250 // Update the timemodifiedoffline field.
1251 $attemptobj->set_offline_modified_time($timenow);
1252 $attemptobj->process_auto_save($timenow);
1253 $transaction->allow_commit();
1255 $result = array();
1256 $result['status'] = true;
1257 $result['warnings'] = $warnings;
1258 return $result;
1262 * Describes the save_attempt return value.
1264 * @return external_single_structure
1265 * @since Moodle 3.1
1267 public static function save_attempt_returns() {
1268 return new external_single_structure(
1269 array(
1270 'status' => new external_value(PARAM_BOOL, 'status: true if success'),
1271 'warnings' => new external_warnings(),
1277 * Describes the parameters for process_attempt.
1279 * @return external_function_parameters
1280 * @since Moodle 3.1
1282 public static function process_attempt_parameters() {
1283 return new external_function_parameters (
1284 array(
1285 'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1286 'data' => new external_multiple_structure(
1287 new external_single_structure(
1288 array(
1289 'name' => new external_value(PARAM_RAW, 'data name'),
1290 'value' => new external_value(PARAM_RAW, 'data value'),
1293 'the data to be saved', VALUE_DEFAULT, array()
1295 'finishattempt' => new external_value(PARAM_BOOL, 'whether to finish or not the attempt', VALUE_DEFAULT, false),
1296 'timeup' => new external_value(PARAM_BOOL, 'whether the WS was called by a timer when the time is up',
1297 VALUE_DEFAULT, false),
1298 'preflightdata' => new external_multiple_structure(
1299 new external_single_structure(
1300 array(
1301 'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
1302 'value' => new external_value(PARAM_RAW, 'data value'),
1304 ), 'Preflight required data (like passwords)', VALUE_DEFAULT, array()
1311 * Process responses during an attempt at a quiz and also deals with attempts finishing.
1313 * @param int $attemptid attempt id
1314 * @param array $data the data to be saved
1315 * @param bool $finishattempt whether to finish or not the attempt
1316 * @param bool $timeup whether the WS was called by a timer when the time is up
1317 * @param array $preflightdata preflight required data (like passwords)
1318 * @return array of warnings and the attempt state after the processing
1319 * @since Moodle 3.1
1321 public static function process_attempt($attemptid, $data, $finishattempt = false, $timeup = false, $preflightdata = array()) {
1322 global $USER;
1324 $warnings = array();
1326 $params = array(
1327 'attemptid' => $attemptid,
1328 'data' => $data,
1329 'finishattempt' => $finishattempt,
1330 'timeup' => $timeup,
1331 'preflightdata' => $preflightdata,
1333 $params = self::validate_parameters(self::process_attempt_parameters(), $params);
1335 // Do not check access manager rules and evaluate fail if overdue.
1336 $attemptobj = quiz_attempt::create($params['attemptid']);
1337 $failifoverdue = !($attemptobj->get_quizobj()->get_quiz()->overduehandling == 'graceperiod');
1339 list($attemptobj, $messages) = self::validate_attempt($params, false, $failifoverdue);
1341 // Prevent functions like file_get_submitted_draft_itemid() or form library requiring a sesskey for WS requests.
1342 if (WS_SERVER || PHPUNIT_TEST) {
1343 $USER->ignoresesskey = true;
1345 // Create the $_POST object required by the question engine.
1346 $_POST = array();
1347 foreach ($params['data'] as $element) {
1348 $_POST[$element['name']] = $element['value'];
1349 $_REQUEST[$element['name']] = $element['value'];
1351 $timenow = time();
1352 $finishattempt = $params['finishattempt'];
1353 $timeup = $params['timeup'];
1355 $result = array();
1356 // Update the timemodifiedoffline field.
1357 $attemptobj->set_offline_modified_time($timenow);
1358 $result['state'] = $attemptobj->process_attempt($timenow, $finishattempt, $timeup, 0);
1360 $result['warnings'] = $warnings;
1361 return $result;
1365 * Describes the process_attempt return value.
1367 * @return external_single_structure
1368 * @since Moodle 3.1
1370 public static function process_attempt_returns() {
1371 return new external_single_structure(
1372 array(
1373 'state' => new external_value(PARAM_ALPHANUMEXT, 'state: the new attempt state:
1374 inprogress, finished, overdue, abandoned'),
1375 'warnings' => new external_warnings(),
1381 * Validate an attempt finished for review. The attempt would be reviewed by a user or a teacher.
1383 * @param array $params Array of parameters including the attemptid
1384 * @return array containing the attempt object and display options
1385 * @since Moodle 3.1
1386 * @throws moodle_exception
1387 * @throws moodle_quiz_exception
1389 protected static function validate_attempt_review($params) {
1391 $attemptobj = quiz_attempt::create($params['attemptid']);
1392 $attemptobj->check_review_capability();
1394 $displayoptions = $attemptobj->get_display_options(true);
1395 if ($attemptobj->is_own_attempt()) {
1396 if (!$attemptobj->is_finished()) {
1397 throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'attemptclosed');
1398 } else if (!$displayoptions->attempt) {
1399 throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'noreview', null, '',
1400 $attemptobj->cannot_review_message());
1402 } else if (!$attemptobj->is_review_allowed()) {
1403 throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'noreviewattempt');
1405 return array($attemptobj, $displayoptions);
1409 * Describes the parameters for get_attempt_review.
1411 * @return external_function_parameters
1412 * @since Moodle 3.1
1414 public static function get_attempt_review_parameters() {
1415 return new external_function_parameters (
1416 array(
1417 'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1418 'page' => new external_value(PARAM_INT, 'page number, empty for all the questions in all the pages',
1419 VALUE_DEFAULT, -1),
1425 * Returns review information for the given finished attempt, can be used by users or teachers.
1427 * @param int $attemptid attempt id
1428 * @param int $page page number, empty for all the questions in all the pages
1429 * @return array of warnings and the attempt data, feedback and questions
1430 * @since Moodle 3.1
1431 * @throws moodle_exception
1432 * @throws moodle_quiz_exception
1434 public static function get_attempt_review($attemptid, $page = -1) {
1435 global $PAGE;
1437 $warnings = array();
1439 $params = array(
1440 'attemptid' => $attemptid,
1441 'page' => $page,
1443 $params = self::validate_parameters(self::get_attempt_review_parameters(), $params);
1445 list($attemptobj, $displayoptions) = self::validate_attempt_review($params);
1447 if ($params['page'] !== -1) {
1448 $page = $attemptobj->force_page_number_into_range($params['page']);
1449 } else {
1450 $page = 'all';
1453 // Prepare the output.
1454 $result = array();
1455 $result['attempt'] = $attemptobj->get_attempt();
1456 $result['questions'] = self::get_attempt_questions_data($attemptobj, true, $page, true);
1458 $result['additionaldata'] = array();
1459 // Summary data (from behaviours).
1460 $summarydata = $attemptobj->get_additional_summary_data($displayoptions);
1461 foreach ($summarydata as $key => $data) {
1462 // This text does not need formatting (no need for external_format_[string|text]).
1463 $result['additionaldata'][] = array(
1464 'id' => $key,
1465 'title' => $data['title'], $attemptobj->get_quizobj()->get_context()->id,
1466 'content' => $data['content'],
1470 // Feedback if there is any, and the user is allowed to see it now.
1471 $grade = quiz_rescale_grade($attemptobj->get_attempt()->sumgrades, $attemptobj->get_quiz(), false);
1473 $feedback = $attemptobj->get_overall_feedback($grade);
1474 if ($displayoptions->overallfeedback && $feedback) {
1475 $result['additionaldata'][] = array(
1476 'id' => 'feedback',
1477 'title' => get_string('feedback', 'quiz'),
1478 'content' => $feedback,
1482 $result['grade'] = $grade;
1483 $result['warnings'] = $warnings;
1484 return $result;
1488 * Describes the get_attempt_review return value.
1490 * @return external_single_structure
1491 * @since Moodle 3.1
1493 public static function get_attempt_review_returns() {
1494 return new external_single_structure(
1495 array(
1496 'grade' => new external_value(PARAM_RAW, 'grade for the quiz (or empty or "notyetgraded")'),
1497 'attempt' => self::attempt_structure(),
1498 'additionaldata' => new external_multiple_structure(
1499 new external_single_structure(
1500 array(
1501 'id' => new external_value(PARAM_ALPHANUMEXT, 'id of the data'),
1502 'title' => new external_value(PARAM_TEXT, 'data title'),
1503 'content' => new external_value(PARAM_RAW, 'data content'),
1507 'questions' => new external_multiple_structure(self::question_structure()),
1508 'warnings' => new external_warnings(),
1514 * Describes the parameters for view_attempt.
1516 * @return external_function_parameters
1517 * @since Moodle 3.1
1519 public static function view_attempt_parameters() {
1520 return new external_function_parameters (
1521 array(
1522 'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1523 'page' => new external_value(PARAM_INT, 'page number'),
1524 'preflightdata' => new external_multiple_structure(
1525 new external_single_structure(
1526 array(
1527 'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
1528 'value' => new external_value(PARAM_RAW, 'data value'),
1530 ), 'Preflight required data (like passwords)', VALUE_DEFAULT, array()
1537 * Trigger the attempt viewed event.
1539 * @param int $attemptid attempt id
1540 * @param int $page page number
1541 * @param array $preflightdata preflight required data (like passwords)
1542 * @return array of warnings and status result
1543 * @since Moodle 3.1
1545 public static function view_attempt($attemptid, $page, $preflightdata = array()) {
1547 $warnings = array();
1549 $params = array(
1550 'attemptid' => $attemptid,
1551 'page' => $page,
1552 'preflightdata' => $preflightdata,
1554 $params = self::validate_parameters(self::view_attempt_parameters(), $params);
1555 list($attemptobj, $messages) = self::validate_attempt($params);
1557 // Log action.
1558 $attemptobj->fire_attempt_viewed_event();
1560 // Update attempt page, throwing an exception if $page is not valid.
1561 if (!$attemptobj->set_currentpage($params['page'])) {
1562 throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'Out of sequence access');
1565 $result = array();
1566 $result['status'] = true;
1567 $result['warnings'] = $warnings;
1568 return $result;
1572 * Describes the view_attempt return value.
1574 * @return external_single_structure
1575 * @since Moodle 3.1
1577 public static function view_attempt_returns() {
1578 return new external_single_structure(
1579 array(
1580 'status' => new external_value(PARAM_BOOL, 'status: true if success'),
1581 'warnings' => new external_warnings(),
1587 * Describes the parameters for view_attempt_summary.
1589 * @return external_function_parameters
1590 * @since Moodle 3.1
1592 public static function view_attempt_summary_parameters() {
1593 return new external_function_parameters (
1594 array(
1595 'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1596 'preflightdata' => new external_multiple_structure(
1597 new external_single_structure(
1598 array(
1599 'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
1600 'value' => new external_value(PARAM_RAW, 'data value'),
1602 ), 'Preflight required data (like passwords)', VALUE_DEFAULT, array()
1609 * Trigger the attempt summary viewed event.
1611 * @param int $attemptid attempt id
1612 * @param array $preflightdata preflight required data (like passwords)
1613 * @return array of warnings and status result
1614 * @since Moodle 3.1
1616 public static function view_attempt_summary($attemptid, $preflightdata = array()) {
1618 $warnings = array();
1620 $params = array(
1621 'attemptid' => $attemptid,
1622 'preflightdata' => $preflightdata,
1624 $params = self::validate_parameters(self::view_attempt_summary_parameters(), $params);
1625 list($attemptobj, $messages) = self::validate_attempt($params);
1627 // Log action.
1628 $attemptobj->fire_attempt_summary_viewed_event();
1630 $result = array();
1631 $result['status'] = true;
1632 $result['warnings'] = $warnings;
1633 return $result;
1637 * Describes the view_attempt_summary return value.
1639 * @return external_single_structure
1640 * @since Moodle 3.1
1642 public static function view_attempt_summary_returns() {
1643 return new external_single_structure(
1644 array(
1645 'status' => new external_value(PARAM_BOOL, 'status: true if success'),
1646 'warnings' => new external_warnings(),
1652 * Describes the parameters for view_attempt_review.
1654 * @return external_function_parameters
1655 * @since Moodle 3.1
1657 public static function view_attempt_review_parameters() {
1658 return new external_function_parameters (
1659 array(
1660 'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1666 * Trigger the attempt reviewed event.
1668 * @param int $attemptid attempt id
1669 * @return array of warnings and status result
1670 * @since Moodle 3.1
1672 public static function view_attempt_review($attemptid) {
1674 $warnings = array();
1676 $params = array(
1677 'attemptid' => $attemptid,
1679 $params = self::validate_parameters(self::view_attempt_review_parameters(), $params);
1680 list($attemptobj, $displayoptions) = self::validate_attempt_review($params);
1682 // Log action.
1683 $attemptobj->fire_attempt_reviewed_event();
1685 $result = array();
1686 $result['status'] = true;
1687 $result['warnings'] = $warnings;
1688 return $result;
1692 * Describes the view_attempt_review return value.
1694 * @return external_single_structure
1695 * @since Moodle 3.1
1697 public static function view_attempt_review_returns() {
1698 return new external_single_structure(
1699 array(
1700 'status' => new external_value(PARAM_BOOL, 'status: true if success'),
1701 'warnings' => new external_warnings(),
1707 * Describes the parameters for view_quiz.
1709 * @return external_function_parameters
1710 * @since Moodle 3.1
1712 public static function get_quiz_feedback_for_grade_parameters() {
1713 return new external_function_parameters (
1714 array(
1715 'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
1716 'grade' => new external_value(PARAM_FLOAT, 'the grade to check'),
1722 * Get the feedback text that should be show to a student who got the given grade in the given quiz.
1724 * @param int $quizid quiz instance id
1725 * @param float $grade the grade to check
1726 * @return array of warnings and status result
1727 * @since Moodle 3.1
1728 * @throws moodle_exception
1730 public static function get_quiz_feedback_for_grade($quizid, $grade) {
1731 global $DB;
1733 $params = array(
1734 'quizid' => $quizid,
1735 'grade' => $grade,
1737 $params = self::validate_parameters(self::get_quiz_feedback_for_grade_parameters(), $params);
1738 $warnings = array();
1740 list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
1742 $result = array();
1743 $result['feedbacktext'] = '';
1744 $result['feedbacktextformat'] = FORMAT_MOODLE;
1746 $feedback = quiz_feedback_record_for_grade($params['grade'], $quiz);
1747 if (!empty($feedback->feedbacktext)) {
1748 list($text, $format) = external_format_text($feedback->feedbacktext, $feedback->feedbacktextformat, $context->id,
1749 'mod_quiz', 'feedback', $feedback->id);
1750 $result['feedbacktext'] = $text;
1751 $result['feedbacktextformat'] = $format;
1752 $feedbackinlinefiles = external_util::get_area_files($context->id, 'mod_quiz', 'feedback', $feedback->id);
1753 if (!empty($feedbackinlinefiles)) {
1754 $result['feedbackinlinefiles'] = $feedbackinlinefiles;
1758 $result['warnings'] = $warnings;
1759 return $result;
1763 * Describes the get_quiz_feedback_for_grade return value.
1765 * @return external_single_structure
1766 * @since Moodle 3.1
1768 public static function get_quiz_feedback_for_grade_returns() {
1769 return new external_single_structure(
1770 array(
1771 'feedbacktext' => new external_value(PARAM_RAW, 'the comment that corresponds to this grade (empty for none)'),
1772 'feedbacktextformat' => new external_format_value('feedbacktext', VALUE_OPTIONAL),
1773 'feedbackinlinefiles' => new external_files('feedback inline files', VALUE_OPTIONAL),
1774 'warnings' => new external_warnings(),
1780 * Describes the parameters for get_quiz_access_information.
1782 * @return external_function_parameters
1783 * @since Moodle 3.1
1785 public static function get_quiz_access_information_parameters() {
1786 return new external_function_parameters (
1787 array(
1788 'quizid' => new external_value(PARAM_INT, 'quiz instance id')
1794 * Return access information for a given quiz.
1796 * @param int $quizid quiz instance id
1797 * @return array of warnings and the access information
1798 * @since Moodle 3.1
1799 * @throws moodle_quiz_exception
1801 public static function get_quiz_access_information($quizid) {
1802 global $DB, $USER;
1804 $warnings = array();
1806 $params = array(
1807 'quizid' => $quizid
1809 $params = self::validate_parameters(self::get_quiz_access_information_parameters(), $params);
1811 list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
1813 $result = array();
1814 // Capabilities first.
1815 $result['canattempt'] = has_capability('mod/quiz:attempt', $context);;
1816 $result['canmanage'] = has_capability('mod/quiz:manage', $context);;
1817 $result['canpreview'] = has_capability('mod/quiz:preview', $context);;
1818 $result['canreviewmyattempts'] = has_capability('mod/quiz:reviewmyattempts', $context);;
1819 $result['canviewreports'] = has_capability('mod/quiz:viewreports', $context);;
1821 // Access manager now.
1822 $quizobj = quiz::create($cm->instance, $USER->id);
1823 $ignoretimelimits = has_capability('mod/quiz:ignoretimelimits', $context, null, false);
1824 $timenow = time();
1825 $accessmanager = new quiz_access_manager($quizobj, $timenow, $ignoretimelimits);
1827 $result['accessrules'] = $accessmanager->describe_rules();
1828 $result['activerulenames'] = $accessmanager->get_active_rule_names();
1829 $result['preventaccessreasons'] = $accessmanager->prevent_access();
1831 $result['warnings'] = $warnings;
1832 return $result;
1836 * Describes the get_quiz_access_information return value.
1838 * @return external_single_structure
1839 * @since Moodle 3.1
1841 public static function get_quiz_access_information_returns() {
1842 return new external_single_structure(
1843 array(
1844 'canattempt' => new external_value(PARAM_BOOL, 'Whether the user can do the quiz or not.'),
1845 'canmanage' => new external_value(PARAM_BOOL, 'Whether the user can edit the quiz settings or not.'),
1846 'canpreview' => new external_value(PARAM_BOOL, 'Whether the user can preview the quiz or not.'),
1847 'canreviewmyattempts' => new external_value(PARAM_BOOL, 'Whether the users can review their previous attempts
1848 or not.'),
1849 'canviewreports' => new external_value(PARAM_BOOL, 'Whether the user can view the quiz reports or not.'),
1850 'accessrules' => new external_multiple_structure(
1851 new external_value(PARAM_TEXT, 'rule description'), 'list of rules'),
1852 'activerulenames' => new external_multiple_structure(
1853 new external_value(PARAM_PLUGIN, 'rule plugin names'), 'list of active rules'),
1854 'preventaccessreasons' => new external_multiple_structure(
1855 new external_value(PARAM_TEXT, 'access restriction description'), 'list of reasons'),
1856 'warnings' => new external_warnings(),
1862 * Describes the parameters for get_attempt_access_information.
1864 * @return external_function_parameters
1865 * @since Moodle 3.1
1867 public static function get_attempt_access_information_parameters() {
1868 return new external_function_parameters (
1869 array(
1870 'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
1871 'attemptid' => new external_value(PARAM_INT, 'attempt id, 0 for the user last attempt if exists', VALUE_DEFAULT, 0),
1877 * Return access information for a given attempt in a quiz.
1879 * @param int $quizid quiz instance id
1880 * @param int $attemptid attempt id, 0 for the user last attempt if exists
1881 * @return array of warnings and the access information
1882 * @since Moodle 3.1
1883 * @throws moodle_quiz_exception
1885 public static function get_attempt_access_information($quizid, $attemptid = 0) {
1886 global $DB, $USER;
1888 $warnings = array();
1890 $params = array(
1891 'quizid' => $quizid,
1892 'attemptid' => $attemptid,
1894 $params = self::validate_parameters(self::get_attempt_access_information_parameters(), $params);
1896 list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
1898 $attempttocheck = 0;
1899 if (!empty($params['attemptid'])) {
1900 $attemptobj = quiz_attempt::create($params['attemptid']);
1901 if ($attemptobj->get_userid() != $USER->id) {
1902 throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'notyourattempt');
1904 $attempttocheck = $attemptobj->get_attempt();
1907 // Access manager now.
1908 $quizobj = quiz::create($cm->instance, $USER->id);
1909 $ignoretimelimits = has_capability('mod/quiz:ignoretimelimits', $context, null, false);
1910 $timenow = time();
1911 $accessmanager = new quiz_access_manager($quizobj, $timenow, $ignoretimelimits);
1913 $attempts = quiz_get_user_attempts($quiz->id, $USER->id, 'finished', true);
1914 $lastfinishedattempt = end($attempts);
1915 if ($unfinishedattempt = quiz_get_user_attempt_unfinished($quiz->id, $USER->id)) {
1916 $attempts[] = $unfinishedattempt;
1918 // Check if the attempt is now overdue. In that case the state will change.
1919 $quizobj->create_attempt_object($unfinishedattempt)->handle_if_time_expired(time(), false);
1921 if ($unfinishedattempt->state != quiz_attempt::IN_PROGRESS and $unfinishedattempt->state != quiz_attempt::OVERDUE) {
1922 $lastfinishedattempt = $unfinishedattempt;
1925 $numattempts = count($attempts);
1927 if (!$attempttocheck) {
1928 $attempttocheck = $unfinishedattempt ? $unfinishedattempt : $lastfinishedattempt;
1931 $result = array();
1932 $result['isfinished'] = $accessmanager->is_finished($numattempts, $lastfinishedattempt);
1933 $result['preventnewattemptreasons'] = $accessmanager->prevent_new_attempt($numattempts, $lastfinishedattempt);
1935 if ($attempttocheck) {
1936 $endtime = $accessmanager->get_end_time($attempttocheck);
1937 $result['endtime'] = ($endtime === false) ? 0 : $endtime;
1938 $attemptid = $unfinishedattempt ? $unfinishedattempt->id : null;
1939 $result['ispreflightcheckrequired'] = $accessmanager->is_preflight_check_required($attemptid);
1942 $result['warnings'] = $warnings;
1943 return $result;
1947 * Describes the get_attempt_access_information return value.
1949 * @return external_single_structure
1950 * @since Moodle 3.1
1952 public static function get_attempt_access_information_returns() {
1953 return new external_single_structure(
1954 array(
1955 'endtime' => new external_value(PARAM_INT, 'When the attempt must be submitted (determined by rules).',
1956 VALUE_OPTIONAL),
1957 'isfinished' => new external_value(PARAM_BOOL, 'Whether there is no way the user will ever be allowed to attempt.'),
1958 'ispreflightcheckrequired' => new external_value(PARAM_BOOL, 'whether a check is required before the user
1959 starts/continues his attempt.', VALUE_OPTIONAL),
1960 'preventnewattemptreasons' => new external_multiple_structure(
1961 new external_value(PARAM_TEXT, 'access restriction description'),
1962 'list of reasons'),
1963 'warnings' => new external_warnings(),
1969 * Describes the parameters for get_quiz_required_qtypes.
1971 * @return external_function_parameters
1972 * @since Moodle 3.1
1974 public static function get_quiz_required_qtypes_parameters() {
1975 return new external_function_parameters (
1976 array(
1977 'quizid' => new external_value(PARAM_INT, 'quiz instance id')
1983 * Return the potential question types that would be required for a given quiz.
1984 * Please note that for random question types we return the potential question types in the category choosen.
1986 * @param int $quizid quiz instance id
1987 * @return array of warnings and the access information
1988 * @since Moodle 3.1
1989 * @throws moodle_quiz_exception
1991 public static function get_quiz_required_qtypes($quizid) {
1992 global $DB, $USER;
1994 $warnings = array();
1996 $params = array(
1997 'quizid' => $quizid
1999 $params = self::validate_parameters(self::get_quiz_required_qtypes_parameters(), $params);
2001 list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
2003 $quizobj = quiz::create($cm->instance, $USER->id);
2004 $quizobj->preload_questions();
2005 $quizobj->load_questions();
2007 // Question types used.
2008 $result = array();
2009 $result['questiontypes'] = $quizobj->get_all_question_types_used(true);
2010 $result['warnings'] = $warnings;
2011 return $result;
2015 * Describes the get_quiz_required_qtypes return value.
2017 * @return external_single_structure
2018 * @since Moodle 3.1
2020 public static function get_quiz_required_qtypes_returns() {
2021 return new external_single_structure(
2022 array(
2023 'questiontypes' => new external_multiple_structure(
2024 new external_value(PARAM_PLUGIN, 'question type'), 'list of question types used in the quiz'),
2025 'warnings' => new external_warnings(),