MDL-77564 Quiz display options: Hide or show the grade information
[moodle.git] / mod / quiz / classes / external.php
blobd5b291f63e519c0d101f4ede08409b0b14190156
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 use core_course\external\helper_for_get_mods_by_courses;
28 use core_external\external_api;
29 use core_external\external_files;
30 use core_external\external_format_value;
31 use core_external\external_function_parameters;
32 use core_external\external_multiple_structure;
33 use core_external\external_single_structure;
34 use core_external\external_value;
35 use core_external\external_warnings;
36 use core_external\util;
37 use mod_quiz\access_manager;
38 use mod_quiz\quiz_attempt;
39 use mod_quiz\quiz_settings;
41 defined('MOODLE_INTERNAL') || die;
43 require_once($CFG->dirroot . '/mod/quiz/locallib.php');
45 /**
46 * Quiz external functions
48 * @package mod_quiz
49 * @category external
50 * @copyright 2016 Juan Leyva <juan@moodle.com>
51 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
52 * @since Moodle 3.1
54 class mod_quiz_external extends external_api {
56 /**
57 * Describes the parameters for get_quizzes_by_courses.
59 * @return external_function_parameters
60 * @since Moodle 3.1
62 public static function get_quizzes_by_courses_parameters() {
63 return new external_function_parameters (
65 'courseids' => new external_multiple_structure(
66 new external_value(PARAM_INT, 'course id'), 'Array of course ids', VALUE_DEFAULT, []
72 /**
73 * Returns a list of quizzes in a provided list of courses,
74 * if no list is provided all quizzes that the user can view will be returned.
76 * @param array $courseids Array of course ids
77 * @return array of quizzes details
78 * @since Moodle 3.1
80 public static function get_quizzes_by_courses($courseids = []) {
81 global $USER;
83 $warnings = [];
84 $returnedquizzes = [];
86 $params = [
87 'courseids' => $courseids,
89 $params = self::validate_parameters(self::get_quizzes_by_courses_parameters(), $params);
91 $mycourses = [];
92 if (empty($params['courseids'])) {
93 $mycourses = enrol_get_my_courses();
94 $params['courseids'] = array_keys($mycourses);
97 // Ensure there are courseids to loop through.
98 if (!empty($params['courseids'])) {
100 list($courses, $warnings) = util::validate_courses($params['courseids'], $mycourses);
102 // Get the quizzes in this course, this function checks users visibility permissions.
103 // We can avoid then additional validate_context calls.
104 $quizzes = get_all_instances_in_courses("quiz", $courses);
105 foreach ($quizzes as $quiz) {
106 $context = context_module::instance($quiz->coursemodule);
108 // Update quiz with override information.
109 $quiz = quiz_update_effective_access($quiz, $USER->id);
111 // Entry to return.
112 $quizdetails = helper_for_get_mods_by_courses::standard_coursemodule_element_values(
113 $quiz, 'mod_quiz', 'mod/quiz:view', 'mod/quiz:view');
115 if (has_capability('mod/quiz:view', $context)) {
116 $quizdetails['introfiles'] = util::get_area_files($context->id, 'mod_quiz', 'intro', false, false);
117 $viewablefields = ['timeopen', 'timeclose', 'attempts', 'timelimit', 'grademethod', 'decimalpoints',
118 'questiondecimalpoints', 'sumgrades', 'grade', 'preferredbehaviour'];
120 // Sometimes this function returns just empty.
121 $hasfeedback = quiz_has_feedback($quiz);
122 $quizdetails['hasfeedback'] = (!empty($hasfeedback)) ? 1 : 0;
124 $timenow = time();
125 $quizobj = quiz_settings::create($quiz->id, $USER->id);
126 $accessmanager = new access_manager($quizobj, $timenow, has_capability('mod/quiz:ignoretimelimits',
127 $context, null, false));
129 // Fields the user could see if have access to the quiz.
130 if (!$accessmanager->prevent_access()) {
131 $quizdetails['hasquestions'] = (int) $quizobj->has_questions();
132 $quizdetails['autosaveperiod'] = get_config('quiz', 'autosaveperiod');
134 $additionalfields = ['attemptonlast', 'reviewattempt', 'reviewcorrectness', 'reviewmaxmarks', 'reviewmarks',
135 'reviewspecificfeedback', 'reviewgeneralfeedback', 'reviewrightanswer',
136 'reviewoverallfeedback', 'questionsperpage', 'navmethod',
137 'browsersecurity', 'delay1', 'delay2', 'showuserpicture', 'showblocks',
138 'completionattemptsexhausted', 'overduehandling',
139 'graceperiod', 'canredoquestions', 'allowofflineattempts'];
140 $viewablefields = array_merge($viewablefields, $additionalfields);
142 // Any course module fields that previously existed in quiz.
143 $quizdetails['completionpass'] = $quizobj->get_cm()->completionpassgrade;
146 // Fields only for managers.
147 if (has_capability('moodle/course:manageactivities', $context)) {
148 $additionalfields = ['shuffleanswers', 'timecreated', 'timemodified', 'password', 'subnet'];
149 $viewablefields = array_merge($viewablefields, $additionalfields);
152 foreach ($viewablefields as $field) {
153 $quizdetails[$field] = $quiz->{$field};
156 $returnedquizzes[] = $quizdetails;
159 $result = [];
160 $result['quizzes'] = $returnedquizzes;
161 $result['warnings'] = $warnings;
162 return $result;
166 * Describes the get_quizzes_by_courses return value.
168 * @return external_single_structure
169 * @since Moodle 3.1
171 public static function get_quizzes_by_courses_returns() {
172 return new external_single_structure(
174 'quizzes' => new external_multiple_structure(
175 new external_single_structure(array_merge(
176 helper_for_get_mods_by_courses::standard_coursemodule_elements_returns(true),
178 'timeopen' => new external_value(PARAM_INT, 'The time when this quiz opens. (0 = no restriction.)',
179 VALUE_OPTIONAL),
180 'timeclose' => new external_value(PARAM_INT, 'The time when this quiz closes. (0 = no restriction.)',
181 VALUE_OPTIONAL),
182 'timelimit' => new external_value(PARAM_INT, 'The time limit for quiz attempts, in seconds.',
183 VALUE_OPTIONAL),
184 'overduehandling' => new external_value(PARAM_ALPHA, 'The method used to handle overdue attempts.
185 \'autosubmit\', \'graceperiod\' or \'autoabandon\'.',
186 VALUE_OPTIONAL),
187 'graceperiod' => new external_value(PARAM_INT, 'The amount of time (in seconds) after the time limit
188 runs out during which attempts can still be submitted,
189 if overduehandling is set to allow it.', VALUE_OPTIONAL),
190 'preferredbehaviour' => new external_value(PARAM_ALPHANUMEXT, 'The behaviour to ask questions to use.',
191 VALUE_OPTIONAL),
192 'canredoquestions' => new external_value(PARAM_INT, 'Allows students to redo any completed question
193 within a quiz attempt.', VALUE_OPTIONAL),
194 'attempts' => new external_value(PARAM_INT, 'The maximum number of attempts a student is allowed.',
195 VALUE_OPTIONAL),
196 'attemptonlast' => new external_value(PARAM_INT, 'Whether subsequent attempts start from the answer
197 to the previous attempt (1) or start blank (0).',
198 VALUE_OPTIONAL),
199 'grademethod' => new external_value(PARAM_INT, 'One of the values QUIZ_GRADEHIGHEST, QUIZ_GRADEAVERAGE,
200 QUIZ_ATTEMPTFIRST or QUIZ_ATTEMPTLAST.', VALUE_OPTIONAL),
201 'decimalpoints' => new external_value(PARAM_INT, 'Number of decimal points to use when displaying
202 grades.', VALUE_OPTIONAL),
203 'questiondecimalpoints' => new external_value(PARAM_INT, 'Number of decimal points to use when
204 displaying question grades.
205 (-1 means use decimalpoints.)', VALUE_OPTIONAL),
206 'reviewattempt' => new external_value(PARAM_INT, 'Whether users are allowed to review their quiz
207 attempts at various times. This is a bit field, decoded by the
208 \mod_quiz\question\display_options class. It is formed by ORing
209 together the constants defined there.', VALUE_OPTIONAL),
210 'reviewcorrectness' => new external_value(PARAM_INT, 'Whether users are allowed to review their quiz
211 attempts at various times.A bit field, like reviewattempt.', VALUE_OPTIONAL),
212 'reviewmaxmarks' => new external_value(PARAM_INT, 'Whether users are allowed to review their quiz
213 attempts at various times. A bit field, like reviewattempt.', VALUE_OPTIONAL),
214 'reviewmarks' => new external_value(PARAM_INT, 'Whether users are allowed to review their quiz attempts
215 at various times. A bit field, like reviewattempt.',
216 VALUE_OPTIONAL),
217 'reviewspecificfeedback' => new external_value(PARAM_INT, 'Whether users are allowed to review their
218 quiz attempts at various times. A bit field, like
219 reviewattempt.', VALUE_OPTIONAL),
220 'reviewgeneralfeedback' => new external_value(PARAM_INT, 'Whether users are allowed to review their
221 quiz attempts at various times. A bit field, like
222 reviewattempt.', VALUE_OPTIONAL),
223 'reviewrightanswer' => new external_value(PARAM_INT, 'Whether users are allowed to review their quiz
224 attempts at various times. A bit field, like
225 reviewattempt.', VALUE_OPTIONAL),
226 'reviewoverallfeedback' => new external_value(PARAM_INT, 'Whether users are allowed to review their quiz
227 attempts at various times. A bit field, like
228 reviewattempt.', VALUE_OPTIONAL),
229 'questionsperpage' => new external_value(PARAM_INT, 'How often to insert a page break when editing
230 the quiz, or when shuffling the question order.',
231 VALUE_OPTIONAL),
232 'navmethod' => new external_value(PARAM_ALPHA, 'Any constraints on how the user is allowed to navigate
233 around the quiz. Currently recognised values are
234 \'free\' and \'seq\'.', VALUE_OPTIONAL),
235 'shuffleanswers' => new external_value(PARAM_INT, 'Whether the parts of the question should be shuffled,
236 in those question types that support it.', VALUE_OPTIONAL),
237 'sumgrades' => new external_value(PARAM_FLOAT, 'The total of all the question instance maxmarks.',
238 VALUE_OPTIONAL),
239 'grade' => new external_value(PARAM_FLOAT, 'The total that the quiz overall grade is scaled to be
240 out of.', VALUE_OPTIONAL),
241 'timecreated' => new external_value(PARAM_INT, 'The time when the quiz was added to the course.',
242 VALUE_OPTIONAL),
243 'timemodified' => new external_value(PARAM_INT, 'Last modified time.',
244 VALUE_OPTIONAL),
245 'password' => new external_value(PARAM_RAW, 'A password that the student must enter before starting or
246 continuing a quiz attempt.', VALUE_OPTIONAL),
247 'subnet' => new external_value(PARAM_RAW, 'Used to restrict the IP addresses from which this quiz can
248 be attempted. The format is as requried by the address_in_subnet
249 function.', VALUE_OPTIONAL),
250 'browsersecurity' => new external_value(PARAM_ALPHANUMEXT, 'Restriciton on the browser the student must
251 use. E.g. \'securewindow\'.', VALUE_OPTIONAL),
252 'delay1' => new external_value(PARAM_INT, 'Delay that must be left between the first and second attempt,
253 in seconds.', VALUE_OPTIONAL),
254 'delay2' => new external_value(PARAM_INT, 'Delay that must be left between the second and subsequent
255 attempt, in seconds.', VALUE_OPTIONAL),
256 'showuserpicture' => new external_value(PARAM_INT, 'Option to show the user\'s picture during the
257 attempt and on the review page.', VALUE_OPTIONAL),
258 'showblocks' => new external_value(PARAM_INT, 'Whether blocks should be shown on the attempt.php and
259 review.php pages.', VALUE_OPTIONAL),
260 'completionattemptsexhausted' => new external_value(PARAM_INT, 'Mark quiz complete when the student has
261 exhausted the maximum number of attempts',
262 VALUE_OPTIONAL),
263 'completionpass' => new external_value(PARAM_INT, 'Whether to require passing grade', VALUE_OPTIONAL),
264 'allowofflineattempts' => new external_value(PARAM_INT, 'Whether to allow the quiz to be attempted
265 offline in the mobile app', VALUE_OPTIONAL),
266 'autosaveperiod' => new external_value(PARAM_INT, 'Auto-save delay', VALUE_OPTIONAL),
267 'hasfeedback' => new external_value(PARAM_INT, 'Whether the quiz has any non-blank feedback text',
268 VALUE_OPTIONAL),
269 'hasquestions' => new external_value(PARAM_INT, 'Whether the quiz has questions', VALUE_OPTIONAL),
273 'warnings' => new external_warnings(),
280 * Utility function for validating a quiz.
282 * @param int $quizid quiz instance id
283 * @return array array containing the quiz, course, context and course module objects
284 * @since Moodle 3.1
286 protected static function validate_quiz($quizid) {
287 global $DB;
289 // Request and permission validation.
290 $quiz = $DB->get_record('quiz', ['id' => $quizid], '*', MUST_EXIST);
291 list($course, $cm) = get_course_and_cm_from_instance($quiz, 'quiz');
293 $context = context_module::instance($cm->id);
294 self::validate_context($context);
296 return [$quiz, $course, $cm, $context];
300 * Describes the parameters for view_quiz.
302 * @return external_function_parameters
303 * @since Moodle 3.1
305 public static function view_quiz_parameters() {
306 return new external_function_parameters (
308 'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
314 * Trigger the course module viewed event and update the module completion status.
316 * @param int $quizid quiz instance id
317 * @return array of warnings and status result
318 * @since Moodle 3.1
320 public static function view_quiz($quizid) {
321 global $DB;
323 $params = self::validate_parameters(self::view_quiz_parameters(), ['quizid' => $quizid]);
324 $warnings = [];
326 list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
328 // Trigger course_module_viewed event and completion.
329 quiz_view($quiz, $course, $cm, $context);
331 $result = [];
332 $result['status'] = true;
333 $result['warnings'] = $warnings;
334 return $result;
338 * Describes the view_quiz return value.
340 * @return external_single_structure
341 * @since Moodle 3.1
343 public static function view_quiz_returns() {
344 return new external_single_structure(
346 'status' => new external_value(PARAM_BOOL, 'status: true if success'),
347 'warnings' => new external_warnings(),
353 * Describes the parameters for get_user_attempts.
355 * @return external_function_parameters
356 * @since Moodle 3.1
358 public static function get_user_attempts_parameters() {
359 return new external_function_parameters (
361 'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
362 'userid' => new external_value(PARAM_INT, 'user id, empty for current user', VALUE_DEFAULT, 0),
363 'status' => new external_value(PARAM_ALPHA, 'quiz status: all, finished or unfinished', VALUE_DEFAULT, 'finished'),
364 'includepreviews' => new external_value(PARAM_BOOL, 'whether to include previews or not', VALUE_DEFAULT, false),
371 * Return a list of attempts for the given quiz and user.
373 * @param int $quizid quiz instance id
374 * @param int $userid user id
375 * @param string $status quiz status: all, finished or unfinished
376 * @param bool $includepreviews whether to include previews or not
377 * @return array of warnings and the list of attempts
378 * @since Moodle 3.1
380 public static function get_user_attempts($quizid, $userid = 0, $status = 'finished', $includepreviews = false) {
381 global $USER;
383 $warnings = [];
385 $params = [
386 'quizid' => $quizid,
387 'userid' => $userid,
388 'status' => $status,
389 'includepreviews' => $includepreviews,
391 $params = self::validate_parameters(self::get_user_attempts_parameters(), $params);
393 list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
395 if (!in_array($params['status'], ['all', 'finished', 'unfinished'])) {
396 throw new invalid_parameter_exception('Invalid status value');
399 // Default value for userid.
400 if (empty($params['userid'])) {
401 $params['userid'] = $USER->id;
404 $user = core_user::get_user($params['userid'], '*', MUST_EXIST);
405 core_user::require_active_user($user);
407 // Extra checks so only users with permissions can view other users attempts.
408 if ($USER->id != $user->id) {
409 require_capability('mod/quiz:viewreports', $context);
412 // Update quiz with override information.
413 $quiz = quiz_update_effective_access($quiz, $params['userid']);
414 $attempts = quiz_get_user_attempts($quiz->id, $user->id, $params['status'], $params['includepreviews']);
415 $attemptresponse = [];
416 foreach ($attempts as $attempt) {
417 $reviewoptions = quiz_get_review_options($quiz, $attempt, $context);
418 if (!has_capability('mod/quiz:viewreports', $context) &&
419 ($reviewoptions->marks < question_display_options::MARK_AND_MAX || $attempt->state != quiz_attempt::FINISHED)) {
420 // Blank the mark if the teacher does not allow it.
421 $attempt->sumgrades = null;
423 $attemptresponse[] = $attempt;
425 $result = [];
426 $result['attempts'] = $attemptresponse;
427 $result['warnings'] = $warnings;
428 return $result;
432 * Describes a single attempt structure.
434 * @return external_single_structure the attempt structure
436 private static function attempt_structure() {
437 return new external_single_structure(
439 'id' => new external_value(PARAM_INT, 'Attempt id.', VALUE_OPTIONAL),
440 'quiz' => new external_value(PARAM_INT, 'Foreign key reference to the quiz that was attempted.',
441 VALUE_OPTIONAL),
442 'userid' => new external_value(PARAM_INT, 'Foreign key reference to the user whose attempt this is.',
443 VALUE_OPTIONAL),
444 'attempt' => new external_value(PARAM_INT, 'Sequentially numbers this students attempts at this quiz.',
445 VALUE_OPTIONAL),
446 'uniqueid' => new external_value(PARAM_INT, 'Foreign key reference to the question_usage that holds the
447 details of the the question_attempts that make up this quiz
448 attempt.', VALUE_OPTIONAL),
449 'layout' => new external_value(PARAM_RAW, 'Attempt layout.', VALUE_OPTIONAL),
450 'currentpage' => new external_value(PARAM_INT, 'Attempt current page.', VALUE_OPTIONAL),
451 'preview' => new external_value(PARAM_INT, 'Whether is a preview attempt or not.', VALUE_OPTIONAL),
452 'state' => new external_value(PARAM_ALPHA, 'The current state of the attempts. \'inprogress\',
453 \'overdue\', \'finished\' or \'abandoned\'.', VALUE_OPTIONAL),
454 'timestart' => new external_value(PARAM_INT, 'Time when the attempt was started.', VALUE_OPTIONAL),
455 'timefinish' => new external_value(PARAM_INT, 'Time when the attempt was submitted.
456 0 if the attempt has not been submitted yet.', VALUE_OPTIONAL),
457 'timemodified' => new external_value(PARAM_INT, 'Last modified time.', VALUE_OPTIONAL),
458 'timemodifiedoffline' => new external_value(PARAM_INT, 'Last modified time via webservices.', VALUE_OPTIONAL),
459 'timecheckstate' => new external_value(PARAM_INT, 'Next time quiz cron should check attempt for
460 state changes. NULL means never check.', VALUE_OPTIONAL),
461 'sumgrades' => new external_value(PARAM_FLOAT, 'Total marks for this attempt.', VALUE_OPTIONAL),
462 'gradednotificationsenttime' => new external_value(PARAM_INT,
463 'Time when the student was notified that manual grading of their attempt was complete.', VALUE_OPTIONAL),
469 * Describes the get_user_attempts return value.
471 * @return external_single_structure
472 * @since Moodle 3.1
474 public static function get_user_attempts_returns() {
475 return new external_single_structure(
477 'attempts' => new external_multiple_structure(self::attempt_structure()),
478 'warnings' => new external_warnings(),
484 * Describes the parameters for get_user_best_grade.
486 * @return external_function_parameters
487 * @since Moodle 3.1
489 public static function get_user_best_grade_parameters() {
490 return new external_function_parameters (
492 'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
493 'userid' => new external_value(PARAM_INT, 'user id', VALUE_DEFAULT, 0),
499 * Get the best current grade for the given user on a quiz.
501 * @param int $quizid quiz instance id
502 * @param int $userid user id
503 * @return array of warnings and the grade information
504 * @since Moodle 3.1
506 public static function get_user_best_grade($quizid, $userid = 0) {
507 global $DB, $USER, $CFG;
508 require_once($CFG->libdir . '/gradelib.php');
510 $warnings = [];
512 $params = [
513 'quizid' => $quizid,
514 'userid' => $userid,
516 $params = self::validate_parameters(self::get_user_best_grade_parameters(), $params);
518 list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
520 // Default value for userid.
521 if (empty($params['userid'])) {
522 $params['userid'] = $USER->id;
525 $user = core_user::get_user($params['userid'], '*', MUST_EXIST);
526 core_user::require_active_user($user);
528 // Extra checks so only users with permissions can view other users attempts.
529 if ($USER->id != $user->id) {
530 require_capability('mod/quiz:viewreports', $context);
533 $result = [];
535 // This code was mostly copied from mod/quiz/view.php. We need to make the web service logic consistent.
536 // Get this user's attempts.
537 $attempts = quiz_get_user_attempts($quiz->id, $user->id, 'all');
538 $canviewgrade = false;
539 if ($attempts) {
540 if ($USER->id != $user->id) {
541 // No need to check the permission here. We did it at by require_capability('mod/quiz:viewreports', $context).
542 $canviewgrade = true;
543 } else {
544 // Work out which columns we need, taking account what data is available in each attempt.
545 [$notused, $alloptions] = quiz_get_combined_reviewoptions($quiz, $attempts);
546 $canviewgrade = $alloptions->marks >= question_display_options::MARK_AND_MAX;
550 $grade = $canviewgrade ? quiz_get_best_grade($quiz, $user->id) : null;
552 if ($grade === null) {
553 $result['hasgrade'] = false;
554 } else {
555 $result['hasgrade'] = true;
556 $result['grade'] = $grade;
559 // Inform user of the grade to pass if non-zero.
560 $gradinginfo = grade_get_grades($course->id, 'mod', 'quiz', $quiz->id, $user->id);
561 if (!empty($gradinginfo->items)) {
562 $item = $gradinginfo->items[0];
564 if ($item && grade_floats_different($item->gradepass, 0)) {
565 $result['gradetopass'] = $item->gradepass;
569 $result['warnings'] = $warnings;
570 return $result;
574 * Describes the get_user_best_grade return value.
576 * @return external_single_structure
577 * @since Moodle 3.1
579 public static function get_user_best_grade_returns() {
580 return new external_single_structure(
582 'hasgrade' => new external_value(PARAM_BOOL, 'Whether the user has a grade on the given quiz.'),
583 'grade' => new external_value(PARAM_FLOAT, 'The grade (only if the user has a grade).', VALUE_OPTIONAL),
584 'gradetopass' => new external_value(PARAM_FLOAT, 'The grade to pass the quiz (only if set).', VALUE_OPTIONAL),
585 'warnings' => new external_warnings(),
591 * Describes the parameters for get_combined_review_options.
593 * @return external_function_parameters
594 * @since Moodle 3.1
596 public static function get_combined_review_options_parameters() {
597 return new external_function_parameters (
599 'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
600 'userid' => new external_value(PARAM_INT, 'user id (empty for current user)', VALUE_DEFAULT, 0),
607 * Combines the review options from a number of different quiz attempts.
609 * @param int $quizid quiz instance id
610 * @param int $userid user id (empty for current user)
611 * @return array of warnings and the review options
612 * @since Moodle 3.1
614 public static function get_combined_review_options($quizid, $userid = 0) {
615 global $DB, $USER;
617 $warnings = [];
619 $params = [
620 'quizid' => $quizid,
621 'userid' => $userid,
623 $params = self::validate_parameters(self::get_combined_review_options_parameters(), $params);
625 list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
627 // Default value for userid.
628 if (empty($params['userid'])) {
629 $params['userid'] = $USER->id;
632 $user = core_user::get_user($params['userid'], '*', MUST_EXIST);
633 core_user::require_active_user($user);
635 // Extra checks so only users with permissions can view other users attempts.
636 if ($USER->id != $user->id) {
637 require_capability('mod/quiz:viewreports', $context);
640 $attempts = quiz_get_user_attempts($quiz->id, $user->id, 'all', true);
642 $result = [];
643 $result['someoptions'] = [];
644 $result['alloptions'] = [];
646 list($someoptions, $alloptions) = quiz_get_combined_reviewoptions($quiz, $attempts);
648 foreach (['someoptions', 'alloptions'] as $typeofoption) {
649 foreach ($$typeofoption as $key => $value) {
650 $result[$typeofoption][] = [
651 "name" => $key,
652 "value" => (!empty($value)) ? $value : 0
657 $result['warnings'] = $warnings;
658 return $result;
662 * Describes the get_combined_review_options return value.
664 * @return external_single_structure
665 * @since Moodle 3.1
667 public static function get_combined_review_options_returns() {
668 return new external_single_structure(
670 'someoptions' => new external_multiple_structure(
671 new external_single_structure(
673 'name' => new external_value(PARAM_ALPHANUMEXT, 'option name'),
674 'value' => new external_value(PARAM_INT, 'option value'),
678 'alloptions' => new external_multiple_structure(
679 new external_single_structure(
681 'name' => new external_value(PARAM_ALPHANUMEXT, 'option name'),
682 'value' => new external_value(PARAM_INT, 'option value'),
686 'warnings' => new external_warnings(),
692 * Describes the parameters for start_attempt.
694 * @return external_function_parameters
695 * @since Moodle 3.1
697 public static function start_attempt_parameters() {
698 return new external_function_parameters (
700 'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
701 'preflightdata' => new external_multiple_structure(
702 new external_single_structure(
704 'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
705 'value' => new external_value(PARAM_RAW, 'data value'),
707 ), 'Preflight required data (like passwords)', VALUE_DEFAULT, []
709 'forcenew' => new external_value(PARAM_BOOL, 'Whether to force a new attempt or not.', VALUE_DEFAULT, false),
716 * Starts a new attempt at a quiz.
718 * @param int $quizid quiz instance id
719 * @param array $preflightdata preflight required data (like passwords)
720 * @param bool $forcenew Whether to force a new attempt or not.
721 * @return array of warnings and the attempt basic data
722 * @since Moodle 3.1
724 public static function start_attempt($quizid, $preflightdata = [], $forcenew = false) {
725 global $DB, $USER;
727 $warnings = [];
728 $attempt = [];
730 $params = [
731 'quizid' => $quizid,
732 'preflightdata' => $preflightdata,
733 'forcenew' => $forcenew,
735 $params = self::validate_parameters(self::start_attempt_parameters(), $params);
736 $forcenew = $params['forcenew'];
738 list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
740 $quizobj = quiz_settings::create($cm->instance, $USER->id);
742 // Check questions.
743 if (!$quizobj->has_questions()) {
744 throw new moodle_exception('noquestionsfound', 'quiz', $quizobj->view_url());
747 // Create an object to manage all the other (non-roles) access rules.
748 $timenow = time();
749 $accessmanager = $quizobj->get_access_manager($timenow);
751 // Validate permissions for creating a new attempt and start a new preview attempt if required.
752 list($currentattemptid, $attemptnumber, $lastattempt, $messages, $page) =
753 quiz_validate_new_attempt($quizobj, $accessmanager, $forcenew, -1, false);
755 // Check access.
756 if (!$quizobj->is_preview_user() && $messages) {
757 // Create warnings with the exact messages.
758 foreach ($messages as $message) {
759 $warnings[] = [
760 'item' => 'quiz',
761 'itemid' => $quiz->id,
762 'warningcode' => '1',
763 'message' => clean_text($message, PARAM_TEXT)
766 } else {
767 if ($accessmanager->is_preflight_check_required($currentattemptid)) {
768 // Need to do some checks before allowing the user to continue.
770 $provideddata = [];
771 foreach ($params['preflightdata'] as $data) {
772 $provideddata[$data['name']] = $data['value'];
775 $errors = $accessmanager->validate_preflight_check($provideddata, [], $currentattemptid);
777 if (!empty($errors)) {
778 throw new moodle_exception(array_shift($errors), 'quiz', $quizobj->view_url());
781 // Pre-flight check passed.
782 $accessmanager->notify_preflight_check_passed($currentattemptid);
785 if ($currentattemptid) {
786 if ($lastattempt->state == quiz_attempt::OVERDUE) {
787 throw new moodle_exception('stateoverdue', 'quiz', $quizobj->view_url());
788 } else {
789 throw new moodle_exception('attemptstillinprogress', 'quiz', $quizobj->view_url());
792 $offlineattempt = WS_SERVER ? true : false;
793 $attempt = quiz_prepare_and_start_new_attempt($quizobj, $attemptnumber, $lastattempt, $offlineattempt);
796 $result = [];
797 $result['attempt'] = $attempt;
798 $result['warnings'] = $warnings;
799 return $result;
803 * Describes the start_attempt return value.
805 * @return external_single_structure
806 * @since Moodle 3.1
808 public static function start_attempt_returns() {
809 return new external_single_structure(
811 'attempt' => self::attempt_structure(),
812 'warnings' => new external_warnings(),
818 * Utility function for validating a given attempt
820 * @param array $params array of parameters including the attemptid and preflight data
821 * @param bool $checkaccessrules whether to check the quiz access rules or not
822 * @param bool $failifoverdue whether to return error if the attempt is overdue
823 * @return array containing the attempt object and access messages
824 * @since Moodle 3.1
826 protected static function validate_attempt($params, $checkaccessrules = true, $failifoverdue = true) {
827 global $USER;
829 $attemptobj = quiz_attempt::create($params['attemptid']);
831 $context = context_module::instance($attemptobj->get_cm()->id);
832 self::validate_context($context);
834 // Check that this attempt belongs to this user.
835 if ($attemptobj->get_userid() != $USER->id) {
836 throw new moodle_exception('notyourattempt', 'quiz', $attemptobj->view_url());
839 // General capabilities check.
840 $ispreviewuser = $attemptobj->is_preview_user();
841 if (!$ispreviewuser) {
842 $attemptobj->require_capability('mod/quiz:attempt');
845 // Check the access rules.
846 $accessmanager = $attemptobj->get_access_manager(time());
847 $messages = [];
848 if ($checkaccessrules) {
849 // If the attempt is now overdue, or abandoned, deal with that.
850 $attemptobj->handle_if_time_expired(time(), true);
852 $messages = $accessmanager->prevent_access();
853 if (!$ispreviewuser && $messages) {
854 throw new moodle_exception('attempterror', 'quiz', $attemptobj->view_url());
858 // Attempt closed?.
859 if ($attemptobj->is_finished()) {
860 throw new moodle_exception('attemptalreadyclosed', 'quiz', $attemptobj->view_url());
861 } else if ($failifoverdue && $attemptobj->get_state() == quiz_attempt::OVERDUE) {
862 throw new moodle_exception('stateoverdue', 'quiz', $attemptobj->view_url());
865 // User submitted data (like the quiz password).
866 if ($accessmanager->is_preflight_check_required($attemptobj->get_attemptid())) {
867 $provideddata = [];
868 foreach ($params['preflightdata'] as $data) {
869 $provideddata[$data['name']] = $data['value'];
872 $errors = $accessmanager->validate_preflight_check($provideddata, [], $params['attemptid']);
873 if (!empty($errors)) {
874 throw new moodle_exception(array_shift($errors), 'quiz', $attemptobj->view_url());
876 // Pre-flight check passed.
877 $accessmanager->notify_preflight_check_passed($params['attemptid']);
880 if (isset($params['page'])) {
881 // Check if the page is out of range.
882 if ($params['page'] != $attemptobj->force_page_number_into_range($params['page'])) {
883 throw new moodle_exception('Invalid page number', 'quiz', $attemptobj->view_url());
886 // Prevent out of sequence access.
887 if (!$attemptobj->check_page_access($params['page'])) {
888 throw new moodle_exception('Out of sequence access', 'quiz', $attemptobj->view_url());
891 // Check slots.
892 $slots = $attemptobj->get_slots($params['page']);
894 if (empty($slots)) {
895 throw new moodle_exception('noquestionsfound', 'quiz', $attemptobj->view_url());
899 return [$attemptobj, $messages];
903 * Describes a single question structure.
905 * @return external_single_structure the question data. Some fields may not be returned depending on the quiz display settings.
906 * @since Moodle 3.1
907 * @since Moodle 3.2 blockedbyprevious parameter added.
909 private static function question_structure() {
910 return new external_single_structure(
912 'slot' => new external_value(PARAM_INT, 'slot number'),
913 'type' => new external_value(PARAM_ALPHANUMEXT, 'question type, i.e: multichoice'),
914 'page' => new external_value(PARAM_INT, 'page of the quiz this question appears on'),
915 'questionnumber' => new external_value(PARAM_RAW,
916 'The question number to display for this question, e.g. "7", "i" or "Custom-B)".'),
917 'number' => new external_value(PARAM_INT,
918 'DO NOT USE. Use questionnumber. Only retained for backwards compatibility.', VALUE_OPTIONAL),
919 'html' => new external_value(PARAM_RAW, 'the question rendered'),
920 'responsefileareas' => new external_multiple_structure(
921 new external_single_structure(
923 'area' => new external_value(PARAM_NOTAGS, 'File area name'),
924 'files' => new external_files('Response files for the question', VALUE_OPTIONAL),
926 ), 'Response file areas including files', VALUE_OPTIONAL
928 'sequencecheck' => new external_value(PARAM_INT, 'the number of real steps in this attempt', VALUE_OPTIONAL),
929 'lastactiontime' => new external_value(PARAM_INT, 'the timestamp of the most recent step in this question attempt',
930 VALUE_OPTIONAL),
931 'hasautosavedstep' => new external_value(PARAM_BOOL, 'whether this question attempt has autosaved data',
932 VALUE_OPTIONAL),
933 'flagged' => new external_value(PARAM_BOOL, 'whether the question is flagged or not'),
934 'state' => new external_value(PARAM_ALPHA, 'the state where the question is in.
935 It will not be returned if the user cannot see it due to the quiz display correctness settings.',
936 VALUE_OPTIONAL),
937 'status' => new external_value(PARAM_RAW, 'current formatted state of the question', VALUE_OPTIONAL),
938 'blockedbyprevious' => new external_value(PARAM_BOOL, 'whether the question is blocked by the previous question',
939 VALUE_OPTIONAL),
940 'mark' => new external_value(PARAM_RAW, 'the mark awarded.
941 It will be returned only if the user is allowed to see it.', VALUE_OPTIONAL),
942 'maxmark' => new external_value(PARAM_FLOAT, 'the maximum mark possible for this question attempt.
943 It will be returned only if the user is allowed to see it.', VALUE_OPTIONAL),
944 'settings' => new external_value(PARAM_RAW, 'Question settings (JSON encoded).', VALUE_OPTIONAL),
946 'The question data. Some fields may not be returned depending on the quiz display settings.'
951 * Return questions information for a given attempt.
953 * @param quiz_attempt $attemptobj the quiz attempt object
954 * @param bool $review whether if we are in review mode or not
955 * @param mixed $page string 'all' or integer page number
956 * @return array array of questions including data
958 private static function get_attempt_questions_data(quiz_attempt $attemptobj, $review, $page = 'all') {
959 global $PAGE;
961 $questions = [];
962 $displayoptions = $attemptobj->get_display_options($review);
963 $renderer = $PAGE->get_renderer('mod_quiz');
964 $contextid = $attemptobj->get_quizobj()->get_context()->id;
966 foreach ($attemptobj->get_slots($page) as $slot) {
967 $qtype = $attemptobj->get_question_type_name($slot);
968 $qattempt = $attemptobj->get_question_attempt($slot);
969 $questiondef = $qattempt->get_question(true);
971 // Get response files (for questions like essay that allows attachments).
972 $responsefileareas = [];
973 foreach (question_bank::get_qtype($qtype)->response_file_areas() as $area) {
974 if ($files = $attemptobj->get_question_attempt($slot)->get_last_qt_files($area, $contextid)) {
975 $responsefileareas[$area]['area'] = $area;
976 $responsefileareas[$area]['files'] = [];
978 foreach ($files as $file) {
979 $responsefileareas[$area]['files'][] = [
980 'filename' => $file->get_filename(),
981 'fileurl' => $qattempt->get_response_file_url($file),
982 'filesize' => $file->get_filesize(),
983 'filepath' => $file->get_filepath(),
984 'mimetype' => $file->get_mimetype(),
985 'timemodified' => $file->get_timemodified(),
991 // Check display settings for question.
992 $settings = $questiondef->get_question_definition_for_external_rendering($qattempt, $displayoptions);
994 $question = [
995 'slot' => $slot,
996 'type' => $qtype,
997 'page' => $attemptobj->get_question_page($slot),
998 'questionnumber' => $attemptobj->get_question_number($slot),
999 'flagged' => $attemptobj->is_question_flagged($slot),
1000 'html' => $attemptobj->render_question($slot, $review, $renderer) . $PAGE->requires->get_end_code(),
1001 'responsefileareas' => $responsefileareas,
1002 'sequencecheck' => $qattempt->get_sequence_check_count(),
1003 'lastactiontime' => $qattempt->get_last_step()->get_timecreated(),
1004 'hasautosavedstep' => $qattempt->has_autosaved_step(),
1005 'settings' => !empty($settings) ? json_encode($settings) : null,
1008 if ($question['questionnumber'] === (string) (int) $question['questionnumber']) {
1009 $question['number'] = $question['questionnumber'];
1012 if ($attemptobj->is_real_question($slot)) {
1013 $showcorrectness = $displayoptions->correctness && $qattempt->has_marks();
1014 if ($showcorrectness) {
1015 $question['state'] = (string) $attemptobj->get_question_state($slot);
1017 $question['status'] = $attemptobj->get_question_status($slot, $displayoptions->correctness);
1018 $question['blockedbyprevious'] = $attemptobj->is_blocked_by_previous_question($slot);
1020 if ($displayoptions->marks >= question_display_options::MAX_ONLY) {
1021 $question['maxmark'] = $qattempt->get_max_mark();
1023 if ($displayoptions->marks >= question_display_options::MARK_AND_MAX) {
1024 $question['mark'] = $attemptobj->get_question_mark($slot);
1026 if ($attemptobj->check_page_access($attemptobj->get_question_page($slot), false)) {
1027 $questions[] = $question;
1030 return $questions;
1034 * Describes the parameters for get_attempt_data.
1036 * @return external_function_parameters
1037 * @since Moodle 3.1
1039 public static function get_attempt_data_parameters() {
1040 return new external_function_parameters (
1042 'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1043 'page' => new external_value(PARAM_INT, 'page number'),
1044 'preflightdata' => new external_multiple_structure(
1045 new external_single_structure(
1047 'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
1048 'value' => new external_value(PARAM_RAW, 'data value'),
1050 ), 'Preflight required data (like passwords)', VALUE_DEFAULT, []
1057 * Returns information for the given attempt page for a quiz attempt in progress.
1059 * @param int $attemptid attempt id
1060 * @param int $page page number
1061 * @param array $preflightdata preflight required data (like passwords)
1062 * @return array of warnings and the attempt data, next page, message and questions
1063 * @since Moodle 3.1
1065 public static function get_attempt_data($attemptid, $page, $preflightdata = []) {
1066 global $PAGE;
1068 $warnings = [];
1070 $params = [
1071 'attemptid' => $attemptid,
1072 'page' => $page,
1073 'preflightdata' => $preflightdata,
1075 $params = self::validate_parameters(self::get_attempt_data_parameters(), $params);
1077 [$attemptobj, $messages] = self::validate_attempt($params);
1079 if ($attemptobj->is_last_page($params['page'])) {
1080 $nextpage = -1;
1081 } else {
1082 $nextpage = $params['page'] + 1;
1085 // TODO: Remove the code once the long-term solution (MDL-76728) has been applied.
1086 // Set a default URL to stop the debugging output.
1087 $PAGE->set_url('/fake/url');
1089 $result = [];
1090 $result['attempt'] = $attemptobj->get_attempt();
1091 $result['messages'] = $messages;
1092 $result['nextpage'] = $nextpage;
1093 $result['warnings'] = $warnings;
1094 $result['questions'] = self::get_attempt_questions_data($attemptobj, false, $params['page']);
1096 return $result;
1100 * Describes the get_attempt_data return value.
1102 * @return external_single_structure
1103 * @since Moodle 3.1
1105 public static function get_attempt_data_returns() {
1106 return new external_single_structure(
1108 'attempt' => self::attempt_structure(),
1109 'messages' => new external_multiple_structure(
1110 new external_value(PARAM_TEXT, 'access message'),
1111 'access messages, will only be returned for users with mod/quiz:preview capability,
1112 for other users this method will throw an exception if there are messages'),
1113 'nextpage' => new external_value(PARAM_INT, 'next page number'),
1114 'questions' => new external_multiple_structure(self::question_structure()),
1115 'warnings' => new external_warnings(),
1121 * Describes the parameters for get_attempt_summary.
1123 * @return external_function_parameters
1124 * @since Moodle 3.1
1126 public static function get_attempt_summary_parameters() {
1127 return new external_function_parameters (
1129 'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1130 'preflightdata' => new external_multiple_structure(
1131 new external_single_structure(
1133 'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
1134 'value' => new external_value(PARAM_RAW, 'data value'),
1136 ), 'Preflight required data (like passwords)', VALUE_DEFAULT, []
1143 * Returns a summary of a quiz attempt before it is submitted.
1145 * @param int $attemptid attempt id
1146 * @param int $preflightdata preflight required data (like passwords)
1147 * @return array of warnings and the attempt summary data for each question
1148 * @since Moodle 3.1
1150 public static function get_attempt_summary($attemptid, $preflightdata = []) {
1152 $warnings = [];
1154 $params = [
1155 'attemptid' => $attemptid,
1156 'preflightdata' => $preflightdata,
1158 $params = self::validate_parameters(self::get_attempt_summary_parameters(), $params);
1160 list($attemptobj, $messages) = self::validate_attempt($params, true, false);
1162 $result = [];
1163 $result['warnings'] = $warnings;
1164 $result['questions'] = self::get_attempt_questions_data($attemptobj, false, 'all');
1166 return $result;
1170 * Describes the get_attempt_summary return value.
1172 * @return external_single_structure
1173 * @since Moodle 3.1
1175 public static function get_attempt_summary_returns() {
1176 return new external_single_structure(
1178 'questions' => new external_multiple_structure(self::question_structure()),
1179 'warnings' => new external_warnings(),
1185 * Describes the parameters for save_attempt.
1187 * @return external_function_parameters
1188 * @since Moodle 3.1
1190 public static function save_attempt_parameters() {
1191 return new external_function_parameters (
1193 'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1194 'data' => new external_multiple_structure(
1195 new external_single_structure(
1197 'name' => new external_value(PARAM_RAW, 'data name'),
1198 'value' => new external_value(PARAM_RAW, 'data value'),
1200 ), 'the data to be saved'
1202 'preflightdata' => new external_multiple_structure(
1203 new external_single_structure(
1205 'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
1206 'value' => new external_value(PARAM_RAW, 'data value'),
1208 ), 'Preflight required data (like passwords)', VALUE_DEFAULT, []
1215 * Processes save requests during the quiz. This function is intended for the quiz auto-save feature.
1217 * @param int $attemptid attempt id
1218 * @param array $data the data to be saved
1219 * @param array $preflightdata preflight required data (like passwords)
1220 * @return array of warnings and execution result
1221 * @since Moodle 3.1
1223 public static function save_attempt($attemptid, $data, $preflightdata = []) {
1224 global $DB, $USER;
1226 $warnings = [];
1228 $params = [
1229 'attemptid' => $attemptid,
1230 'data' => $data,
1231 'preflightdata' => $preflightdata,
1233 $params = self::validate_parameters(self::save_attempt_parameters(), $params);
1235 // Add a page, required by validate_attempt.
1236 list($attemptobj, $messages) = self::validate_attempt($params);
1238 // Prevent functions like file_get_submitted_draft_itemid() or form library requiring a sesskey for WS requests.
1239 if (WS_SERVER || PHPUNIT_TEST) {
1240 $USER->ignoresesskey = true;
1242 $transaction = $DB->start_delegated_transaction();
1243 // Create the $_POST object required by the question engine.
1244 $_POST = [];
1245 foreach ($data as $element) {
1246 $_POST[$element['name']] = $element['value'];
1247 // Some deep core functions like file_get_submitted_draft_itemid() also requires $_REQUEST to be filled.
1248 $_REQUEST[$element['name']] = $element['value'];
1250 $timenow = time();
1251 // Update the timemodifiedoffline field.
1252 $attemptobj->set_offline_modified_time($timenow);
1253 $attemptobj->process_auto_save($timenow);
1254 $transaction->allow_commit();
1256 $result = [];
1257 $result['status'] = true;
1258 $result['warnings'] = $warnings;
1259 return $result;
1263 * Describes the save_attempt return value.
1265 * @return external_single_structure
1266 * @since Moodle 3.1
1268 public static function save_attempt_returns() {
1269 return new external_single_structure(
1271 'status' => new external_value(PARAM_BOOL, 'status: true if success'),
1272 'warnings' => new external_warnings(),
1278 * Describes the parameters for process_attempt.
1280 * @return external_function_parameters
1281 * @since Moodle 3.1
1283 public static function process_attempt_parameters() {
1284 return new external_function_parameters (
1286 'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1287 'data' => new external_multiple_structure(
1288 new external_single_structure(
1290 'name' => new external_value(PARAM_RAW, 'data name'),
1291 'value' => new external_value(PARAM_RAW, 'data value'),
1294 'the data to be saved', VALUE_DEFAULT, []
1296 'finishattempt' => new external_value(PARAM_BOOL, 'whether to finish or not the attempt', VALUE_DEFAULT, false),
1297 'timeup' => new external_value(PARAM_BOOL, 'whether the WS was called by a timer when the time is up',
1298 VALUE_DEFAULT, false),
1299 'preflightdata' => new external_multiple_structure(
1300 new external_single_structure(
1302 'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
1303 'value' => new external_value(PARAM_RAW, 'data value'),
1305 ), 'Preflight required data (like passwords)', VALUE_DEFAULT, []
1312 * Process responses during an attempt at a quiz and also deals with attempts finishing.
1314 * @param int $attemptid attempt id
1315 * @param array $data the data to be saved
1316 * @param bool $finishattempt whether to finish or not the attempt
1317 * @param bool $timeup whether the WS was called by a timer when the time is up
1318 * @param array $preflightdata preflight required data (like passwords)
1319 * @return array of warnings and the attempt state after the processing
1320 * @since Moodle 3.1
1322 public static function process_attempt($attemptid, $data, $finishattempt = false, $timeup = false, $preflightdata = []) {
1323 global $USER;
1325 $warnings = [];
1327 $params = [
1328 'attemptid' => $attemptid,
1329 'data' => $data,
1330 'finishattempt' => $finishattempt,
1331 'timeup' => $timeup,
1332 'preflightdata' => $preflightdata,
1334 $params = self::validate_parameters(self::process_attempt_parameters(), $params);
1336 // Do not check access manager rules and evaluate fail if overdue.
1337 $attemptobj = quiz_attempt::create($params['attemptid']);
1338 $failifoverdue = !($attemptobj->get_quizobj()->get_quiz()->overduehandling == 'graceperiod');
1340 list($attemptobj, $messages) = self::validate_attempt($params, false, $failifoverdue);
1342 // Prevent functions like file_get_submitted_draft_itemid() or form library requiring a sesskey for WS requests.
1343 if (WS_SERVER || PHPUNIT_TEST) {
1344 $USER->ignoresesskey = true;
1346 // Create the $_POST object required by the question engine.
1347 $_POST = [];
1348 foreach ($params['data'] as $element) {
1349 $_POST[$element['name']] = $element['value'];
1350 $_REQUEST[$element['name']] = $element['value'];
1352 $timenow = time();
1353 $finishattempt = $params['finishattempt'];
1354 $timeup = $params['timeup'];
1356 $result = [];
1357 // Update the timemodifiedoffline field.
1358 $attemptobj->set_offline_modified_time($timenow);
1359 $result['state'] = $attemptobj->process_attempt($timenow, $finishattempt, $timeup, 0);
1361 $result['warnings'] = $warnings;
1362 return $result;
1366 * Describes the process_attempt return value.
1368 * @return external_single_structure
1369 * @since Moodle 3.1
1371 public static function process_attempt_returns() {
1372 return new external_single_structure(
1374 'state' => new external_value(PARAM_ALPHANUMEXT, 'state: the new attempt state:
1375 inprogress, finished, overdue, abandoned'),
1376 'warnings' => new external_warnings(),
1382 * Validate an attempt finished for review. The attempt would be reviewed by a user or a teacher.
1384 * @param array $params Array of parameters including the attemptid
1385 * @return array containing the attempt object and display options
1386 * @since Moodle 3.1
1388 protected static function validate_attempt_review($params) {
1390 $attemptobj = quiz_attempt::create($params['attemptid']);
1391 $attemptobj->check_review_capability();
1393 $displayoptions = $attemptobj->get_display_options(true);
1394 if ($attemptobj->is_own_attempt()) {
1395 if (!$attemptobj->is_finished()) {
1396 throw new moodle_exception('attemptclosed', 'quiz', $attemptobj->view_url());
1397 } else if (!$displayoptions->attempt) {
1398 throw new moodle_exception('noreview', 'quiz', $attemptobj->view_url(), null,
1399 $attemptobj->cannot_review_message());
1401 } else if (!$attemptobj->is_review_allowed()) {
1402 throw new moodle_exception('noreviewattempt', 'quiz', $attemptobj->view_url());
1404 return [$attemptobj, $displayoptions];
1408 * Describes the parameters for get_attempt_review.
1410 * @return external_function_parameters
1411 * @since Moodle 3.1
1413 public static function get_attempt_review_parameters() {
1414 return new external_function_parameters (
1416 'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1417 'page' => new external_value(PARAM_INT, 'page number, empty for all the questions in all the pages',
1418 VALUE_DEFAULT, -1),
1424 * Returns review information for the given finished attempt, can be used by users or teachers.
1426 * @param int $attemptid attempt id
1427 * @param int $page page number, empty for all the questions in all the pages
1428 * @return array of warnings and the attempt data, feedback and questions
1429 * @since Moodle 3.1
1431 public static function get_attempt_review($attemptid, $page = -1) {
1432 global $PAGE;
1434 $warnings = [];
1436 $params = [
1437 'attemptid' => $attemptid,
1438 'page' => $page,
1440 $params = self::validate_parameters(self::get_attempt_review_parameters(), $params);
1442 list($attemptobj, $displayoptions) = self::validate_attempt_review($params);
1444 if ($params['page'] !== -1) {
1445 $page = $attemptobj->force_page_number_into_range($params['page']);
1446 } else {
1447 $page = 'all';
1450 // Prepare the output.
1451 $result = [];
1452 $result['attempt'] = $attemptobj->get_attempt();
1453 $result['questions'] = self::get_attempt_questions_data($attemptobj, true, $page, true);
1455 $result['additionaldata'] = [];
1456 // Summary data (from behaviours).
1457 $summarydata = $attemptobj->get_additional_summary_data($displayoptions);
1458 foreach ($summarydata as $key => $data) {
1459 // This text does not need formatting (no need for external_format_[string|text]).
1460 $result['additionaldata'][] = [
1461 'id' => $key,
1462 'title' => $data['title'], $attemptobj->get_quizobj()->get_context()->id,
1463 'content' => $data['content'],
1467 // Feedback if there is any, and the user is allowed to see it now.
1468 $grade = quiz_rescale_grade($attemptobj->get_attempt()->sumgrades, $attemptobj->get_quiz(), false);
1470 $feedback = $attemptobj->get_overall_feedback($grade);
1471 if ($displayoptions->overallfeedback && $feedback) {
1472 $result['additionaldata'][] = [
1473 'id' => 'feedback',
1474 'title' => get_string('feedback', 'quiz'),
1475 'content' => $feedback,
1479 $result['grade'] = $grade;
1480 $result['warnings'] = $warnings;
1481 return $result;
1485 * Describes the get_attempt_review return value.
1487 * @return external_single_structure
1488 * @since Moodle 3.1
1490 public static function get_attempt_review_returns() {
1491 return new external_single_structure(
1493 'grade' => new external_value(PARAM_RAW, 'grade for the quiz (or empty or "notyetgraded")'),
1494 'attempt' => self::attempt_structure(),
1495 'additionaldata' => new external_multiple_structure(
1496 new external_single_structure(
1498 'id' => new external_value(PARAM_ALPHANUMEXT, 'id of the data'),
1499 'title' => new external_value(PARAM_TEXT, 'data title'),
1500 'content' => new external_value(PARAM_RAW, 'data content'),
1504 'questions' => new external_multiple_structure(self::question_structure()),
1505 'warnings' => new external_warnings(),
1511 * Describes the parameters for view_attempt.
1513 * @return external_function_parameters
1514 * @since Moodle 3.1
1516 public static function view_attempt_parameters() {
1517 return new external_function_parameters (
1519 'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1520 'page' => new external_value(PARAM_INT, 'page number'),
1521 'preflightdata' => new external_multiple_structure(
1522 new external_single_structure(
1524 'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
1525 'value' => new external_value(PARAM_RAW, 'data value'),
1527 ), 'Preflight required data (like passwords)', VALUE_DEFAULT, []
1534 * Trigger the attempt viewed event.
1536 * @param int $attemptid attempt id
1537 * @param int $page page number
1538 * @param array $preflightdata preflight required data (like passwords)
1539 * @return array of warnings and status result
1540 * @since Moodle 3.1
1542 public static function view_attempt($attemptid, $page, $preflightdata = []) {
1544 $warnings = [];
1546 $params = [
1547 'attemptid' => $attemptid,
1548 'page' => $page,
1549 'preflightdata' => $preflightdata,
1551 $params = self::validate_parameters(self::view_attempt_parameters(), $params);
1552 list($attemptobj, $messages) = self::validate_attempt($params);
1554 // Log action.
1555 $attemptobj->fire_attempt_viewed_event();
1557 // Update attempt page, throwing an exception if $page is not valid.
1558 if (!$attemptobj->set_currentpage($params['page'])) {
1559 throw new moodle_exception('Out of sequence access', 'quiz', $attemptobj->view_url());
1562 $result = [];
1563 $result['status'] = true;
1564 $result['warnings'] = $warnings;
1565 return $result;
1569 * Describes the view_attempt return value.
1571 * @return external_single_structure
1572 * @since Moodle 3.1
1574 public static function view_attempt_returns() {
1575 return new external_single_structure(
1577 'status' => new external_value(PARAM_BOOL, 'status: true if success'),
1578 'warnings' => new external_warnings(),
1584 * Describes the parameters for view_attempt_summary.
1586 * @return external_function_parameters
1587 * @since Moodle 3.1
1589 public static function view_attempt_summary_parameters() {
1590 return new external_function_parameters (
1592 'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1593 'preflightdata' => new external_multiple_structure(
1594 new external_single_structure(
1596 'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
1597 'value' => new external_value(PARAM_RAW, 'data value'),
1599 ), 'Preflight required data (like passwords)', VALUE_DEFAULT, []
1606 * Trigger the attempt summary viewed event.
1608 * @param int $attemptid attempt id
1609 * @param array $preflightdata preflight required data (like passwords)
1610 * @return array of warnings and status result
1611 * @since Moodle 3.1
1613 public static function view_attempt_summary($attemptid, $preflightdata = []) {
1615 $warnings = [];
1617 $params = [
1618 'attemptid' => $attemptid,
1619 'preflightdata' => $preflightdata,
1621 $params = self::validate_parameters(self::view_attempt_summary_parameters(), $params);
1622 list($attemptobj, $messages) = self::validate_attempt($params);
1624 // Log action.
1625 $attemptobj->fire_attempt_summary_viewed_event();
1627 $result = [];
1628 $result['status'] = true;
1629 $result['warnings'] = $warnings;
1630 return $result;
1634 * Describes the view_attempt_summary return value.
1636 * @return external_single_structure
1637 * @since Moodle 3.1
1639 public static function view_attempt_summary_returns() {
1640 return new external_single_structure(
1642 'status' => new external_value(PARAM_BOOL, 'status: true if success'),
1643 'warnings' => new external_warnings(),
1649 * Describes the parameters for view_attempt_review.
1651 * @return external_function_parameters
1652 * @since Moodle 3.1
1654 public static function view_attempt_review_parameters() {
1655 return new external_function_parameters (
1657 'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1663 * Trigger the attempt reviewed event.
1665 * @param int $attemptid attempt id
1666 * @return array of warnings and status result
1667 * @since Moodle 3.1
1669 public static function view_attempt_review($attemptid) {
1671 $warnings = [];
1673 $params = [
1674 'attemptid' => $attemptid,
1676 $params = self::validate_parameters(self::view_attempt_review_parameters(), $params);
1677 list($attemptobj, $displayoptions) = self::validate_attempt_review($params);
1679 // Log action.
1680 $attemptobj->fire_attempt_reviewed_event();
1682 $result = [];
1683 $result['status'] = true;
1684 $result['warnings'] = $warnings;
1685 return $result;
1689 * Describes the view_attempt_review return value.
1691 * @return external_single_structure
1692 * @since Moodle 3.1
1694 public static function view_attempt_review_returns() {
1695 return new external_single_structure(
1697 'status' => new external_value(PARAM_BOOL, 'status: true if success'),
1698 'warnings' => new external_warnings(),
1704 * Describes the parameters for view_quiz.
1706 * @return external_function_parameters
1707 * @since Moodle 3.1
1709 public static function get_quiz_feedback_for_grade_parameters() {
1710 return new external_function_parameters (
1712 'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
1713 'grade' => new external_value(PARAM_FLOAT, 'the grade to check'),
1719 * Get the feedback text that should be show to a student who got the given grade in the given quiz.
1721 * @param int $quizid quiz instance id
1722 * @param float $grade the grade to check
1723 * @return array of warnings and status result
1724 * @since Moodle 3.1
1726 public static function get_quiz_feedback_for_grade($quizid, $grade) {
1727 global $DB;
1729 $params = [
1730 'quizid' => $quizid,
1731 'grade' => $grade,
1733 $params = self::validate_parameters(self::get_quiz_feedback_for_grade_parameters(), $params);
1734 $warnings = [];
1736 list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
1738 $result = [];
1739 $result['feedbacktext'] = '';
1740 $result['feedbacktextformat'] = FORMAT_MOODLE;
1742 $feedback = quiz_feedback_record_for_grade($params['grade'], $quiz);
1743 if (!empty($feedback->feedbacktext)) {
1744 list($text, $format) = \core_external\util::format_text(
1745 $feedback->feedbacktext,
1746 $feedback->feedbacktextformat,
1747 $context,
1748 'mod_quiz',
1749 'feedback',
1750 $feedback->id
1752 $result['feedbacktext'] = $text;
1753 $result['feedbacktextformat'] = $format;
1754 $feedbackinlinefiles = util::get_area_files($context->id, 'mod_quiz', 'feedback', $feedback->id);
1755 if (!empty($feedbackinlinefiles)) {
1756 $result['feedbackinlinefiles'] = $feedbackinlinefiles;
1760 $result['warnings'] = $warnings;
1761 return $result;
1765 * Describes the get_quiz_feedback_for_grade return value.
1767 * @return external_single_structure
1768 * @since Moodle 3.1
1770 public static function get_quiz_feedback_for_grade_returns() {
1771 return new external_single_structure(
1773 'feedbacktext' => new external_value(PARAM_RAW, 'the comment that corresponds to this grade (empty for none)'),
1774 'feedbacktextformat' => new external_format_value('feedbacktext', VALUE_OPTIONAL),
1775 'feedbackinlinefiles' => new external_files('feedback inline files', VALUE_OPTIONAL),
1776 'warnings' => new external_warnings(),
1782 * Describes the parameters for get_quiz_access_information.
1784 * @return external_function_parameters
1785 * @since Moodle 3.1
1787 public static function get_quiz_access_information_parameters() {
1788 return new external_function_parameters (
1790 'quizid' => new external_value(PARAM_INT, 'quiz instance id')
1796 * Return access information for a given quiz.
1798 * @param int $quizid quiz instance id
1799 * @return array of warnings and the access information
1800 * @since Moodle 3.1
1802 public static function get_quiz_access_information($quizid) {
1803 global $DB, $USER;
1805 $warnings = [];
1807 $params = [
1808 'quizid' => $quizid
1810 $params = self::validate_parameters(self::get_quiz_access_information_parameters(), $params);
1812 list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
1814 $result = [];
1815 // Capabilities first.
1816 $result['canattempt'] = has_capability('mod/quiz:attempt', $context);;
1817 $result['canmanage'] = has_capability('mod/quiz:manage', $context);;
1818 $result['canpreview'] = has_capability('mod/quiz:preview', $context);;
1819 $result['canreviewmyattempts'] = has_capability('mod/quiz:reviewmyattempts', $context);;
1820 $result['canviewreports'] = has_capability('mod/quiz:viewreports', $context);;
1822 // Access manager now.
1823 $quizobj = quiz_settings::create($cm->instance, $USER->id);
1824 $ignoretimelimits = has_capability('mod/quiz:ignoretimelimits', $context, null, false);
1825 $timenow = time();
1826 $accessmanager = new access_manager($quizobj, $timenow, $ignoretimelimits);
1828 $result['accessrules'] = $accessmanager->describe_rules();
1829 $result['activerulenames'] = $accessmanager->get_active_rule_names();
1830 $result['preventaccessreasons'] = $accessmanager->prevent_access();
1832 $result['warnings'] = $warnings;
1833 return $result;
1837 * Describes the get_quiz_access_information return value.
1839 * @return external_single_structure
1840 * @since Moodle 3.1
1842 public static function get_quiz_access_information_returns() {
1843 return new external_single_structure(
1845 'canattempt' => new external_value(PARAM_BOOL, 'Whether the user can do the quiz or not.'),
1846 'canmanage' => new external_value(PARAM_BOOL, 'Whether the user can edit the quiz settings or not.'),
1847 'canpreview' => new external_value(PARAM_BOOL, 'Whether the user can preview the quiz or not.'),
1848 'canreviewmyattempts' => new external_value(PARAM_BOOL, 'Whether the users can review their previous attempts
1849 or not.'),
1850 'canviewreports' => new external_value(PARAM_BOOL, 'Whether the user can view the quiz reports or not.'),
1851 'accessrules' => new external_multiple_structure(
1852 new external_value(PARAM_TEXT, 'rule description'), 'list of rules'),
1853 'activerulenames' => new external_multiple_structure(
1854 new external_value(PARAM_PLUGIN, 'rule plugin names'), 'list of active rules'),
1855 'preventaccessreasons' => new external_multiple_structure(
1856 new external_value(PARAM_TEXT, 'access restriction description'), 'list of reasons'),
1857 'warnings' => new external_warnings(),
1863 * Describes the parameters for get_attempt_access_information.
1865 * @return external_function_parameters
1866 * @since Moodle 3.1
1868 public static function get_attempt_access_information_parameters() {
1869 return new external_function_parameters (
1871 'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
1872 'attemptid' => new external_value(PARAM_INT, 'attempt id, 0 for the user last attempt if exists', VALUE_DEFAULT, 0),
1878 * Return access information for a given attempt in a quiz.
1880 * @param int $quizid quiz instance id
1881 * @param int $attemptid attempt id, 0 for the user last attempt if exists
1882 * @return array of warnings and the access information
1883 * @since Moodle 3.1
1885 public static function get_attempt_access_information($quizid, $attemptid = 0) {
1886 global $DB, $USER;
1888 $warnings = [];
1890 $params = [
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 = null;
1899 if (!empty($params['attemptid'])) {
1900 $attemptobj = quiz_attempt::create($params['attemptid']);
1901 if ($attemptobj->get_userid() != $USER->id) {
1902 throw new moodle_exception('notyourattempt', 'quiz', $attemptobj->view_url());
1904 $attempttocheck = $attemptobj->get_attempt();
1907 // Access manager now.
1908 $quizobj = quiz_settings::create($cm->instance, $USER->id);
1909 $ignoretimelimits = has_capability('mod/quiz:ignoretimelimits', $context, null, false);
1910 $timenow = time();
1911 $accessmanager = new 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 ?: $lastfinishedattempt;
1931 $result = [];
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(
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 (
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
1990 public static function get_quiz_required_qtypes($quizid) {
1991 global $DB, $USER;
1993 $warnings = [];
1995 $params = [
1996 'quizid' => $quizid
1998 $params = self::validate_parameters(self::get_quiz_required_qtypes_parameters(), $params);
2000 list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
2002 $quizobj = quiz_settings::create($cm->instance, $USER->id);
2003 $quizobj->preload_questions();
2004 $quizobj->load_questions();
2006 // Question types used.
2007 $result = [];
2008 $result['questiontypes'] = $quizobj->get_all_question_types_used(true);
2009 $result['warnings'] = $warnings;
2010 return $result;
2014 * Describes the get_quiz_required_qtypes return value.
2016 * @return external_single_structure
2017 * @since Moodle 3.1
2019 public static function get_quiz_required_qtypes_returns() {
2020 return new external_single_structure(
2022 'questiontypes' => new external_multiple_structure(
2023 new external_value(PARAM_PLUGIN, 'question type'), 'list of question types used in the quiz'),
2024 'warnings' => new external_warnings(),