2 // This file is part of Moodle - http://moodle.org/
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.
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/>.
22 * @copyright 2016 Juan Leyva <juan@moodle.com>
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
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');
46 * Quiz external functions
50 * @copyright 2016 Juan Leyva <juan@moodle.com>
51 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
54 class mod_quiz_external
extends external_api
{
57 * Describes the parameters for get_quizzes_by_courses.
59 * @return external_function_parameters
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
, []
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
80 public static function get_quizzes_by_courses($courseids = []) {
84 $returnedquizzes = [];
87 'courseids' => $courseids,
89 $params = self
::validate_parameters(self
::get_quizzes_by_courses_parameters(), $params);
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
);
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;
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;
160 $result['quizzes'] = $returnedquizzes;
161 $result['warnings'] = $warnings;
166 * Describes the get_quizzes_by_courses return value.
168 * @return external_single_structure
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.)',
180 'timeclose' => new external_value(PARAM_INT
, 'The time when this quiz closes. (0 = no restriction.)',
182 'timelimit' => new external_value(PARAM_INT
, 'The time limit for quiz attempts, in seconds.',
184 'overduehandling' => new external_value(PARAM_ALPHA
, 'The method used to handle overdue attempts.
185 \'autosubmit\', \'graceperiod\' or \'autoabandon\'.',
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.',
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.',
196 'attemptonlast' => new external_value(PARAM_INT
, 'Whether subsequent attempts start from the answer
197 to the previous attempt (1) or start blank (0).',
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.',
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.',
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.',
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.',
243 'timemodified' => new external_value(PARAM_INT
, 'Last modified time.',
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',
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',
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
286 protected static function validate_quiz($quizid) {
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
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
320 public static function view_quiz($quizid) {
323 $params = self
::validate_parameters(self
::view_quiz_parameters(), ['quizid' => $quizid]);
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);
332 $result['status'] = true;
333 $result['warnings'] = $warnings;
338 * Describes the view_quiz return value.
340 * @return external_single_structure
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
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
380 public static function get_user_attempts($quizid, $userid = 0, $status = 'finished', $includepreviews = false) {
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 $quizobj = new quiz_settings($quiz, $cm, $course);
416 $gradeitemmarks = $quizobj->get_grade_calculator()->compute_grade_item_totals_for_attempts(
417 array_column($attempts, 'uniqueid'));
418 $attemptresponse = [];
419 foreach ($attempts as $attempt) {
420 $reviewoptions = quiz_get_review_options($quiz, $attempt, $context);
421 if (!has_capability('mod/quiz:viewreports', $context) &&
422 ($reviewoptions->marks
< question_display_options
::MARK_AND_MAX ||
$attempt->state
!= quiz_attempt
::FINISHED
)) {
423 // Blank the mark if the teacher does not allow it.
424 $attempt->sumgrades
= null;
425 } else if (isset($gradeitemmarks[$attempt->uniqueid
])) {
426 $attempt->gradeitemmarks
= [];
427 foreach ($gradeitemmarks[$attempt->uniqueid
] as $gradeitem) {
428 $attempt->gradeitemmarks
[] = [
429 'name' => \core_external\util
::format_string($gradeitem->name
, $context),
430 'grade' => $gradeitem->grade
,
431 'maxgrade' => $gradeitem->maxgrade
,
435 $attemptresponse[] = $attempt;
438 $result['attempts'] = $attemptresponse;
439 $result['warnings'] = $warnings;
444 * Describes a single attempt structure.
446 * @return external_single_structure the attempt structure
448 private static function attempt_structure() {
449 return new external_single_structure(
451 'id' => new external_value(PARAM_INT
, 'Attempt id.', VALUE_OPTIONAL
),
452 'quiz' => new external_value(PARAM_INT
, 'Foreign key reference to the quiz that was attempted.',
454 'userid' => new external_value(PARAM_INT
, 'Foreign key reference to the user whose attempt this is.',
456 'attempt' => new external_value(PARAM_INT
, 'Sequentially numbers this students attempts at this quiz.',
458 'uniqueid' => new external_value(PARAM_INT
, 'Foreign key reference to the question_usage that holds the
459 details of the the question_attempts that make up this quiz
460 attempt.', VALUE_OPTIONAL
),
461 'layout' => new external_value(PARAM_RAW
, 'Attempt layout.', VALUE_OPTIONAL
),
462 'currentpage' => new external_value(PARAM_INT
, 'Attempt current page.', VALUE_OPTIONAL
),
463 'preview' => new external_value(PARAM_INT
, 'Whether is a preview attempt or not.', VALUE_OPTIONAL
),
464 'state' => new external_value(PARAM_ALPHA
, 'The current state of the attempts. \'inprogress\',
465 \'overdue\', \'finished\' or \'abandoned\'.', VALUE_OPTIONAL
),
466 'timestart' => new external_value(PARAM_INT
, 'Time when the attempt was started.', VALUE_OPTIONAL
),
467 'timefinish' => new external_value(PARAM_INT
, 'Time when the attempt was submitted.
468 0 if the attempt has not been submitted yet.', VALUE_OPTIONAL
),
469 'timemodified' => new external_value(PARAM_INT
, 'Last modified time.', VALUE_OPTIONAL
),
470 'timemodifiedoffline' => new external_value(PARAM_INT
, 'Last modified time via webservices.', VALUE_OPTIONAL
),
471 'timecheckstate' => new external_value(PARAM_INT
, 'Next time quiz cron should check attempt for
472 state changes. NULL means never check.', VALUE_OPTIONAL
),
473 'sumgrades' => new external_value(PARAM_FLOAT
, 'Total marks for this attempt.', VALUE_OPTIONAL
),
474 'gradeitemmarks' => new external_multiple_structure(
475 new external_single_structure([
476 'name' => new external_value(PARAM_RAW
, 'The name of this grade item.'),
477 'grade' => new external_value(PARAM_FLOAT
, 'The grade this attempt earned for this item.'),
478 'maxgrade' => new external_value(PARAM_FLOAT
, 'The total this grade is out of.'),
479 ], 'The grade for each grade item.'),
480 'If the quiz has additional grades set up, the mark for each grade for this attempt.', VALUE_OPTIONAL
),
481 'gradednotificationsenttime' => new external_value(PARAM_INT
,
482 'Time when the student was notified that manual grading of their attempt was complete.', VALUE_OPTIONAL
),
488 * Describes the get_user_attempts return value.
490 * @return external_single_structure
493 public static function get_user_attempts_returns() {
494 return new external_single_structure(
496 'attempts' => new external_multiple_structure(self
::attempt_structure()),
497 'warnings' => new external_warnings(),
503 * Describes the parameters for get_user_best_grade.
505 * @return external_function_parameters
508 public static function get_user_best_grade_parameters() {
509 return new external_function_parameters (
511 'quizid' => new external_value(PARAM_INT
, 'quiz instance id'),
512 'userid' => new external_value(PARAM_INT
, 'user id', VALUE_DEFAULT
, 0),
518 * Get the best current grade for the given user on a quiz.
520 * @param int $quizid quiz instance id
521 * @param int $userid user id
522 * @return array of warnings and the grade information
525 public static function get_user_best_grade($quizid, $userid = 0) {
526 global $DB, $USER, $CFG;
527 require_once($CFG->libdir
. '/gradelib.php');
535 $params = self
::validate_parameters(self
::get_user_best_grade_parameters(), $params);
537 list($quiz, $course, $cm, $context) = self
::validate_quiz($params['quizid']);
539 // Default value for userid.
540 if (empty($params['userid'])) {
541 $params['userid'] = $USER->id
;
544 $user = core_user
::get_user($params['userid'], '*', MUST_EXIST
);
545 core_user
::require_active_user($user);
547 // Extra checks so only users with permissions can view other users attempts.
548 if ($USER->id
!= $user->id
) {
549 require_capability('mod/quiz:viewreports', $context);
554 // This code was mostly copied from mod/quiz/view.php. We need to make the web service logic consistent.
555 // Get this user's attempts.
556 $attempts = quiz_get_user_attempts($quiz->id
, $user->id
, 'all');
557 $canviewgrade = false;
559 if ($USER->id
!= $user->id
) {
560 // No need to check the permission here. We did it at by require_capability('mod/quiz:viewreports', $context).
561 $canviewgrade = true;
563 // Work out which columns we need, taking account what data is available in each attempt.
564 [$notused, $alloptions] = quiz_get_combined_reviewoptions($quiz, $attempts);
565 $canviewgrade = $alloptions->marks
>= question_display_options
::MARK_AND_MAX
;
569 $grade = $canviewgrade ?
quiz_get_best_grade($quiz, $user->id
) : null;
571 if ($grade === null) {
572 $result['hasgrade'] = false;
574 $result['hasgrade'] = true;
575 $result['grade'] = $grade;
578 // Inform user of the grade to pass if non-zero.
579 $gradinginfo = grade_get_grades($course->id
, 'mod', 'quiz', $quiz->id
, $user->id
);
580 if (!empty($gradinginfo->items
)) {
581 $item = $gradinginfo->items
[0];
583 if ($item && grade_floats_different($item->gradepass
, 0)) {
584 $result['gradetopass'] = $item->gradepass
;
588 $result['warnings'] = $warnings;
593 * Describes the get_user_best_grade return value.
595 * @return external_single_structure
598 public static function get_user_best_grade_returns() {
599 return new external_single_structure(
601 'hasgrade' => new external_value(PARAM_BOOL
, 'Whether the user has a grade on the given quiz.'),
602 'grade' => new external_value(PARAM_FLOAT
, 'The grade (only if the user has a grade).', VALUE_OPTIONAL
),
603 'gradetopass' => new external_value(PARAM_FLOAT
, 'The grade to pass the quiz (only if set).', VALUE_OPTIONAL
),
604 'warnings' => new external_warnings(),
610 * Describes the parameters for get_combined_review_options.
612 * @return external_function_parameters
615 public static function get_combined_review_options_parameters() {
616 return new external_function_parameters (
618 'quizid' => new external_value(PARAM_INT
, 'quiz instance id'),
619 'userid' => new external_value(PARAM_INT
, 'user id (empty for current user)', VALUE_DEFAULT
, 0),
626 * Combines the review options from a number of different quiz attempts.
628 * @param int $quizid quiz instance id
629 * @param int $userid user id (empty for current user)
630 * @return array of warnings and the review options
633 public static function get_combined_review_options($quizid, $userid = 0) {
642 $params = self
::validate_parameters(self
::get_combined_review_options_parameters(), $params);
644 list($quiz, $course, $cm, $context) = self
::validate_quiz($params['quizid']);
646 // Default value for userid.
647 if (empty($params['userid'])) {
648 $params['userid'] = $USER->id
;
651 $user = core_user
::get_user($params['userid'], '*', MUST_EXIST
);
652 core_user
::require_active_user($user);
654 // Extra checks so only users with permissions can view other users attempts.
655 if ($USER->id
!= $user->id
) {
656 require_capability('mod/quiz:viewreports', $context);
659 // Update quiz with override information.
660 $quiz = quiz_update_effective_access($quiz, $params['userid']);
661 $attempts = quiz_get_user_attempts($quiz->id
, $user->id
, 'all', true);
664 $result['someoptions'] = [];
665 $result['alloptions'] = [];
667 list($someoptions, $alloptions) = quiz_get_combined_reviewoptions($quiz, $attempts);
669 foreach (['someoptions', 'alloptions'] as $typeofoption) {
670 foreach ($
$typeofoption as $key => $value) {
671 $result[$typeofoption][] = [
673 "value" => (!empty($value)) ?
$value : 0
678 $result['warnings'] = $warnings;
683 * Describes the get_combined_review_options return value.
685 * @return external_single_structure
688 public static function get_combined_review_options_returns() {
689 return new external_single_structure(
691 'someoptions' => new external_multiple_structure(
692 new external_single_structure(
694 'name' => new external_value(PARAM_ALPHANUMEXT
, 'option name'),
695 'value' => new external_value(PARAM_INT
, 'option value'),
699 'alloptions' => new external_multiple_structure(
700 new external_single_structure(
702 'name' => new external_value(PARAM_ALPHANUMEXT
, 'option name'),
703 'value' => new external_value(PARAM_INT
, 'option value'),
707 'warnings' => new external_warnings(),
713 * Describes the parameters for start_attempt.
715 * @return external_function_parameters
718 public static function start_attempt_parameters() {
719 return new external_function_parameters (
721 'quizid' => new external_value(PARAM_INT
, 'quiz instance id'),
722 'preflightdata' => new external_multiple_structure(
723 new external_single_structure(
725 'name' => new external_value(PARAM_ALPHANUMEXT
, 'data name'),
726 'value' => new external_value(PARAM_RAW
, 'data value'),
728 ), 'Preflight required data (like passwords)', VALUE_DEFAULT
, []
730 'forcenew' => new external_value(PARAM_BOOL
, 'Whether to force a new attempt or not.', VALUE_DEFAULT
, false),
737 * Starts a new attempt at a quiz.
739 * @param int $quizid quiz instance id
740 * @param array $preflightdata preflight required data (like passwords)
741 * @param bool $forcenew Whether to force a new attempt or not.
742 * @return array of warnings and the attempt basic data
745 public static function start_attempt($quizid, $preflightdata = [], $forcenew = false) {
753 'preflightdata' => $preflightdata,
754 'forcenew' => $forcenew,
756 $params = self
::validate_parameters(self
::start_attempt_parameters(), $params);
757 $forcenew = $params['forcenew'];
759 list($quiz, $course, $cm, $context) = self
::validate_quiz($params['quizid']);
761 $quizobj = quiz_settings
::create($cm->instance
, $USER->id
);
764 if (!$quizobj->has_questions()) {
765 throw new moodle_exception('noquestionsfound', 'quiz', $quizobj->view_url());
768 // Create an object to manage all the other (non-roles) access rules.
770 $accessmanager = $quizobj->get_access_manager($timenow);
772 // Validate permissions for creating a new attempt and start a new preview attempt if required.
773 list($currentattemptid, $attemptnumber, $lastattempt, $messages, $page) =
774 quiz_validate_new_attempt($quizobj, $accessmanager, $forcenew, -1, false);
777 if (!$quizobj->is_preview_user() && $messages) {
778 // Create warnings with the exact messages.
779 foreach ($messages as $message) {
782 'itemid' => $quiz->id
,
783 'warningcode' => '1',
784 'message' => clean_text($message, PARAM_TEXT
)
788 if ($accessmanager->is_preflight_check_required($currentattemptid)) {
789 // Need to do some checks before allowing the user to continue.
792 foreach ($params['preflightdata'] as $data) {
793 $provideddata[$data['name']] = $data['value'];
796 $errors = $accessmanager->validate_preflight_check($provideddata, [], $currentattemptid);
798 if (!empty($errors)) {
799 throw new moodle_exception(array_shift($errors), 'quiz', $quizobj->view_url());
802 // Pre-flight check passed.
803 $accessmanager->notify_preflight_check_passed($currentattemptid);
806 if ($currentattemptid) {
807 if ($lastattempt->state
== quiz_attempt
::OVERDUE
) {
808 throw new moodle_exception('stateoverdue', 'quiz', $quizobj->view_url());
810 throw new moodle_exception('attemptstillinprogress', 'quiz', $quizobj->view_url());
813 $offlineattempt = WS_SERVER ?
true : false;
814 $attempt = quiz_prepare_and_start_new_attempt($quizobj, $attemptnumber, $lastattempt, $offlineattempt);
818 $result['attempt'] = $attempt;
819 $result['warnings'] = $warnings;
824 * Describes the start_attempt return value.
826 * @return external_single_structure
829 public static function start_attempt_returns() {
830 return new external_single_structure(
832 'attempt' => self
::attempt_structure(),
833 'warnings' => new external_warnings(),
839 * Utility function for validating a given attempt
841 * @param array $params array of parameters including the attemptid and preflight data
842 * @param bool $checkaccessrules whether to check the quiz access rules or not
843 * @param bool $failifoverdue whether to return error if the attempt is overdue
844 * @return array containing the attempt object and access messages
847 protected static function validate_attempt($params, $checkaccessrules = true, $failifoverdue = true) {
850 $attemptobj = quiz_attempt
::create($params['attemptid']);
852 $context = context_module
::instance($attemptobj->get_cm()->id
);
853 self
::validate_context($context);
855 // Check that this attempt belongs to this user.
856 if ($attemptobj->get_userid() != $USER->id
) {
857 throw new moodle_exception('notyourattempt', 'quiz', $attemptobj->view_url());
860 // General capabilities check.
861 $ispreviewuser = $attemptobj->is_preview_user();
862 if (!$ispreviewuser) {
863 $attemptobj->require_capability('mod/quiz:attempt');
866 // Check the access rules.
867 $accessmanager = $attemptobj->get_access_manager(time());
869 if ($checkaccessrules) {
870 // If the attempt is now overdue, or abandoned, deal with that.
871 $attemptobj->handle_if_time_expired(time(), true);
873 $messages = $accessmanager->prevent_access();
874 if (!$ispreviewuser && $messages) {
875 throw new moodle_exception('attempterror', 'quiz', $attemptobj->view_url());
880 if ($attemptobj->is_finished()) {
881 throw new moodle_exception('attemptalreadyclosed', 'quiz', $attemptobj->view_url());
882 } else if ($failifoverdue && $attemptobj->get_state() == quiz_attempt
::OVERDUE
) {
883 throw new moodle_exception('stateoverdue', 'quiz', $attemptobj->view_url());
886 // User submitted data (like the quiz password).
887 if ($accessmanager->is_preflight_check_required($attemptobj->get_attemptid())) {
889 foreach ($params['preflightdata'] as $data) {
890 $provideddata[$data['name']] = $data['value'];
893 $errors = $accessmanager->validate_preflight_check($provideddata, [], $params['attemptid']);
894 if (!empty($errors)) {
895 throw new moodle_exception(array_shift($errors), 'quiz', $attemptobj->view_url());
897 // Pre-flight check passed.
898 $accessmanager->notify_preflight_check_passed($params['attemptid']);
901 if (isset($params['page'])) {
902 // Check if the page is out of range.
903 if ($params['page'] != $attemptobj->force_page_number_into_range($params['page'])) {
904 throw new moodle_exception('Invalid page number', 'quiz', $attemptobj->view_url());
907 // Prevent out of sequence access.
908 if (!$attemptobj->check_page_access($params['page'])) {
909 throw new moodle_exception('Out of sequence access', 'quiz', $attemptobj->view_url());
913 $slots = $attemptobj->get_slots($params['page']);
916 throw new moodle_exception('noquestionsfound', 'quiz', $attemptobj->view_url());
920 return [$attemptobj, $messages];
924 * Describes a single question structure.
926 * @return external_single_structure the question data. Some fields may not be returned depending on the quiz display settings.
928 * @since Moodle 3.2 blockedbyprevious parameter added.
930 private static function question_structure() {
931 return new external_single_structure(
933 'slot' => new external_value(PARAM_INT
, 'slot number'),
934 'type' => new external_value(PARAM_ALPHANUMEXT
, 'question type, i.e: multichoice'),
935 'page' => new external_value(PARAM_INT
, 'page of the quiz this question appears on'),
936 'questionnumber' => new external_value(PARAM_RAW
,
937 'The question number to display for this question, e.g. "7", "i" or "Custom-B)".'),
938 'number' => new external_value(PARAM_INT
,
939 'DO NOT USE. Use questionnumber. Only retained for backwards compatibility.', VALUE_OPTIONAL
),
940 'html' => new external_value(PARAM_RAW
, 'the question rendered'),
941 'responsefileareas' => new external_multiple_structure(
942 new external_single_structure(
944 'area' => new external_value(PARAM_NOTAGS
, 'File area name'),
945 'files' => new external_files('Response files for the question', VALUE_OPTIONAL
),
947 ), 'Response file areas including files', VALUE_OPTIONAL
949 'sequencecheck' => new external_value(PARAM_INT
, 'the number of real steps in this attempt', VALUE_OPTIONAL
),
950 'lastactiontime' => new external_value(PARAM_INT
, 'the timestamp of the most recent step in this question attempt',
952 'hasautosavedstep' => new external_value(PARAM_BOOL
, 'whether this question attempt has autosaved data',
954 'flagged' => new external_value(PARAM_BOOL
, 'whether the question is flagged or not'),
955 'state' => new external_value(PARAM_ALPHA
, 'the state where the question is in terms of correctness.
956 It will not be returned if the user cannot see it due to the quiz display correctness settings.',
958 'stateclass' => new external_value(PARAM_NOTAGS
,
959 'A machine-readable class name for the state that this question attempt is in, as returned by question_usage_by_activity::get_question_state_class().
960 Always returned.', VALUE_OPTIONAL
),
961 'status' => new external_value(PARAM_RAW
, 'Human readable state of the question.', VALUE_OPTIONAL
),
962 'blockedbyprevious' => new external_value(PARAM_BOOL
, 'whether the question is blocked by the previous question',
964 'mark' => new external_value(PARAM_RAW
, 'the mark awarded.
965 It will be returned only if the user is allowed to see it.', VALUE_OPTIONAL
),
966 'maxmark' => new external_value(PARAM_FLOAT
, 'the maximum mark possible for this question attempt.
967 It will be returned only if the user is allowed to see it.', VALUE_OPTIONAL
),
968 'settings' => new external_value(PARAM_RAW
, 'Question settings (JSON encoded).', VALUE_OPTIONAL
),
970 'The question data. Some fields may not be returned depending on the quiz display settings.'
975 * Return questions information for a given attempt.
977 * @param quiz_attempt $attemptobj the quiz attempt object
978 * @param bool $review whether if we are in review mode or not
979 * @param mixed $page string 'all' or integer page number
980 * @return array array of questions including data
982 private static function get_attempt_questions_data(quiz_attempt
$attemptobj, $review, $page = 'all') {
986 $displayoptions = $attemptobj->get_display_options($review);
987 $renderer = $PAGE->get_renderer('mod_quiz');
988 $contextid = $attemptobj->get_quizobj()->get_context()->id
;
990 foreach ($attemptobj->get_slots($page) as $slot) {
991 $qtype = $attemptobj->get_question_type_name($slot);
992 $qattempt = $attemptobj->get_question_attempt($slot);
993 $questiondef = $qattempt->get_question(true);
995 // Check display settings for question.
996 $settings = $questiondef->get_question_definition_for_external_rendering($qattempt, $displayoptions);
998 // Navigation information.
1001 'page' => $attemptobj->get_question_page($slot),
1002 'questionnumber' => $attemptobj->get_question_number($slot),
1003 'flagged' => $attemptobj->is_question_flagged($slot),
1004 'sequencecheck' => $qattempt->get_sequence_check_count(),
1005 'lastactiontime' => $qattempt->get_last_step()->get_timecreated(),
1006 'hasautosavedstep' => $qattempt->has_autosaved_step(),
1009 if ($question['questionnumber'] === (string) (int) $question['questionnumber']) {
1010 $question['number'] = $question['questionnumber'];
1013 if ($attemptobj->is_real_question($slot)) {
1014 $showcorrectness = $displayoptions->correctness
&& $qattempt->has_marks();
1015 if ($showcorrectness) {
1016 $question['state'] = (string) $attemptobj->get_question_state($slot);
1018 // The stateclass is used for CSS classes but also for the lang strings.
1019 $question['stateclass'] = $attemptobj->get_question_state_class($slot, $displayoptions->correctness
);
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);
1030 // Check access. This is needed especially when sequential navigation is enforced. To prevent the student see "future" questions.
1031 $haveaccess = $attemptobj->check_page_access($attemptobj->get_question_page($slot), false);
1033 $question['type'] = '';
1034 $question['html'] = '';
1037 // For visited pages/questions it is ok to keep data the user already saw.
1038 $questionalreadyseen = $attemptobj->get_currentpage() >= $attemptobj->get_question_page($slot);
1040 // Information when only the user has access to the question at any moment (free navigation) or already seen.
1041 if ($haveaccess ||
$questionalreadyseen) {
1042 // Get response files (for questions like essay that allows attachments).
1043 $responsefileareas = [];
1044 foreach (question_bank
::get_qtype($qtype)->response_file_areas() as $area) {
1045 if ($files = $attemptobj->get_question_attempt($slot)->get_last_qt_files($area, $contextid)) {
1046 $responsefileareas[$area]['area'] = $area;
1047 $responsefileareas[$area]['files'] = [];
1049 foreach ($files as $file) {
1050 $responsefileareas[$area]['files'][] = [
1051 'filename' => $file->get_filename(),
1052 'fileurl' => $qattempt->get_response_file_url($file),
1053 'filesize' => $file->get_filesize(),
1054 'filepath' => $file->get_filepath(),
1055 'mimetype' => $file->get_mimetype(),
1056 'timemodified' => $file->get_timemodified(),
1061 $question['type'] = $qtype;
1062 $question['html'] = $attemptobj->render_question($slot, $review, $renderer) . $PAGE->requires
->get_end_code();
1063 $question['responsefileareas'] = $responsefileareas;
1064 $question['settings'] = !empty($settings) ?
json_encode($settings) : null;
1066 $questions[] = $question;
1072 * Describes the parameters for get_attempt_data.
1074 * @return external_function_parameters
1077 public static function get_attempt_data_parameters() {
1078 return new external_function_parameters (
1080 'attemptid' => new external_value(PARAM_INT
, 'attempt id'),
1081 'page' => new external_value(PARAM_INT
, 'page number'),
1082 'preflightdata' => new external_multiple_structure(
1083 new external_single_structure(
1085 'name' => new external_value(PARAM_ALPHANUMEXT
, 'data name'),
1086 'value' => new external_value(PARAM_RAW
, 'data value'),
1088 ), 'Preflight required data (like passwords)', VALUE_DEFAULT
, []
1095 * Returns information for the given attempt page for a quiz attempt in progress.
1097 * @param int $attemptid attempt id
1098 * @param int $page page number
1099 * @param array $preflightdata preflight required data (like passwords)
1100 * @return array of warnings and the attempt data, next page, message and questions
1103 public static function get_attempt_data($attemptid, $page, $preflightdata = []) {
1109 'attemptid' => $attemptid,
1111 'preflightdata' => $preflightdata,
1113 $params = self
::validate_parameters(self
::get_attempt_data_parameters(), $params);
1115 [$attemptobj, $messages] = self
::validate_attempt($params);
1117 if ($attemptobj->is_last_page($params['page'])) {
1120 $nextpage = $params['page'] +
1;
1123 // TODO: Remove the code once the long-term solution (MDL-76728) has been applied.
1124 // Set a default URL to stop the debugging output.
1125 $PAGE->set_url('/fake/url');
1128 $result['attempt'] = $attemptobj->get_attempt();
1129 $result['messages'] = $messages;
1130 $result['nextpage'] = $nextpage;
1131 $result['warnings'] = $warnings;
1132 $result['questions'] = self
::get_attempt_questions_data($attemptobj, false, $params['page']);
1138 * Describes the get_attempt_data return value.
1140 * @return external_single_structure
1143 public static function get_attempt_data_returns() {
1144 return new external_single_structure(
1146 'attempt' => self
::attempt_structure(),
1147 'messages' => new external_multiple_structure(
1148 new external_value(PARAM_TEXT
, 'access message'),
1149 'access messages, will only be returned for users with mod/quiz:preview capability,
1150 for other users this method will throw an exception if there are messages'),
1151 'nextpage' => new external_value(PARAM_INT
, 'next page number'),
1152 'questions' => new external_multiple_structure(self
::question_structure()),
1153 'warnings' => new external_warnings(),
1159 * Describes the parameters for get_attempt_summary.
1161 * @return external_function_parameters
1164 public static function get_attempt_summary_parameters() {
1165 return new external_function_parameters (
1167 'attemptid' => new external_value(PARAM_INT
, 'attempt id'),
1168 'preflightdata' => new external_multiple_structure(
1169 new external_single_structure(
1171 'name' => new external_value(PARAM_ALPHANUMEXT
, 'data name'),
1172 'value' => new external_value(PARAM_RAW
, 'data value'),
1174 ), 'Preflight required data (like passwords)', VALUE_DEFAULT
, []
1181 * Returns a summary of a quiz attempt before it is submitted.
1183 * @param int $attemptid attempt id
1184 * @param int $preflightdata preflight required data (like passwords)
1185 * @return array of warnings and the attempt summary data for each question
1188 public static function get_attempt_summary($attemptid, $preflightdata = []) {
1193 'attemptid' => $attemptid,
1194 'preflightdata' => $preflightdata,
1196 $params = self
::validate_parameters(self
::get_attempt_summary_parameters(), $params);
1198 list($attemptobj, $messages) = self
::validate_attempt($params, true, false);
1201 $result['warnings'] = $warnings;
1202 $result['questions'] = self
::get_attempt_questions_data($attemptobj, false, 'all');
1204 if ($attemptobj->get_state() == quiz_attempt
::IN_PROGRESS
&& $attemptobj->get_quiz()->navmethod
== 'free') {
1205 // Only count the unanswered question if the navigation method is set to free.
1206 $result['totalunanswered'] = $attemptobj->get_number_of_unanswered_questions();
1214 * Describes the get_attempt_summary return value.
1216 * @return external_single_structure
1219 public static function get_attempt_summary_returns() {
1220 return new external_single_structure(
1222 'questions' => new external_multiple_structure(self
::question_structure()),
1223 'totalunanswered' => new external_value(PARAM_INT
, 'Total unanswered questions.', VALUE_OPTIONAL
),
1224 'warnings' => new external_warnings(),
1230 * Describes the parameters for save_attempt.
1232 * @return external_function_parameters
1235 public static function save_attempt_parameters() {
1236 return new external_function_parameters (
1238 'attemptid' => new external_value(PARAM_INT
, 'attempt id'),
1239 'data' => new external_multiple_structure(
1240 new external_single_structure(
1242 'name' => new external_value(PARAM_RAW
, 'data name'),
1243 'value' => new external_value(PARAM_RAW
, 'data value'),
1245 ), 'the data to be saved'
1247 'preflightdata' => new external_multiple_structure(
1248 new external_single_structure(
1250 'name' => new external_value(PARAM_ALPHANUMEXT
, 'data name'),
1251 'value' => new external_value(PARAM_RAW
, 'data value'),
1253 ), 'Preflight required data (like passwords)', VALUE_DEFAULT
, []
1260 * Processes save requests during the quiz. This function is intended for the quiz auto-save feature.
1262 * @param int $attemptid attempt id
1263 * @param array $data the data to be saved
1264 * @param array $preflightdata preflight required data (like passwords)
1265 * @return array of warnings and execution result
1268 public static function save_attempt($attemptid, $data, $preflightdata = []) {
1274 'attemptid' => $attemptid,
1276 'preflightdata' => $preflightdata,
1278 $params = self
::validate_parameters(self
::save_attempt_parameters(), $params);
1280 // Add a page, required by validate_attempt.
1281 list($attemptobj, $messages) = self
::validate_attempt($params);
1283 // Prevent functions like file_get_submitted_draft_itemid() or form library requiring a sesskey for WS requests.
1284 if (WS_SERVER || PHPUNIT_TEST
) {
1285 $USER->ignoresesskey
= true;
1287 $transaction = $DB->start_delegated_transaction();
1288 // Create the $_POST object required by the question engine.
1290 foreach ($data as $element) {
1291 $_POST[$element['name']] = $element['value'];
1292 // Some deep core functions like file_get_submitted_draft_itemid() also requires $_REQUEST to be filled.
1293 $_REQUEST[$element['name']] = $element['value'];
1296 // Update the timemodifiedoffline field.
1297 $attemptobj->set_offline_modified_time($timenow);
1298 $attemptobj->process_auto_save($timenow);
1299 $transaction->allow_commit();
1302 $result['status'] = true;
1303 $result['warnings'] = $warnings;
1308 * Describes the save_attempt return value.
1310 * @return external_single_structure
1313 public static function save_attempt_returns() {
1314 return new external_single_structure(
1316 'status' => new external_value(PARAM_BOOL
, 'status: true if success'),
1317 'warnings' => new external_warnings(),
1323 * Describes the parameters for process_attempt.
1325 * @return external_function_parameters
1328 public static function process_attempt_parameters() {
1329 return new external_function_parameters (
1331 'attemptid' => new external_value(PARAM_INT
, 'attempt id'),
1332 'data' => new external_multiple_structure(
1333 new external_single_structure(
1335 'name' => new external_value(PARAM_RAW
, 'data name'),
1336 'value' => new external_value(PARAM_RAW
, 'data value'),
1339 'the data to be saved', VALUE_DEFAULT
, []
1341 'finishattempt' => new external_value(PARAM_BOOL
, 'whether to finish or not the attempt', VALUE_DEFAULT
, false),
1342 'timeup' => new external_value(PARAM_BOOL
, 'whether the WS was called by a timer when the time is up',
1343 VALUE_DEFAULT
, false),
1344 'preflightdata' => new external_multiple_structure(
1345 new external_single_structure(
1347 'name' => new external_value(PARAM_ALPHANUMEXT
, 'data name'),
1348 'value' => new external_value(PARAM_RAW
, 'data value'),
1350 ), 'Preflight required data (like passwords)', VALUE_DEFAULT
, []
1357 * Process responses during an attempt at a quiz and also deals with attempts finishing.
1359 * @param int $attemptid attempt id
1360 * @param array $data the data to be saved
1361 * @param bool $finishattempt whether to finish or not the attempt
1362 * @param bool $timeup whether the WS was called by a timer when the time is up
1363 * @param array $preflightdata preflight required data (like passwords)
1364 * @return array of warnings and the attempt state after the processing
1367 public static function process_attempt($attemptid, $data, $finishattempt = false, $timeup = false, $preflightdata = []) {
1373 'attemptid' => $attemptid,
1375 'finishattempt' => $finishattempt,
1376 'timeup' => $timeup,
1377 'preflightdata' => $preflightdata,
1379 $params = self
::validate_parameters(self
::process_attempt_parameters(), $params);
1381 // Do not check access manager rules and evaluate fail if overdue.
1382 $attemptobj = quiz_attempt
::create($params['attemptid']);
1383 $failifoverdue = !($attemptobj->get_quizobj()->get_quiz()->overduehandling
== 'graceperiod');
1385 list($attemptobj, $messages) = self
::validate_attempt($params, false, $failifoverdue);
1387 // Prevent functions like file_get_submitted_draft_itemid() or form library requiring a sesskey for WS requests.
1388 if (WS_SERVER || PHPUNIT_TEST
) {
1389 $USER->ignoresesskey
= true;
1391 // Create the $_POST object required by the question engine.
1393 foreach ($params['data'] as $element) {
1394 $_POST[$element['name']] = $element['value'];
1395 $_REQUEST[$element['name']] = $element['value'];
1398 $finishattempt = $params['finishattempt'];
1399 $timeup = $params['timeup'];
1402 // Update the timemodifiedoffline field.
1403 $attemptobj->set_offline_modified_time($timenow);
1404 $result['state'] = $attemptobj->process_attempt($timenow, $finishattempt, $timeup, 0);
1406 $result['warnings'] = $warnings;
1411 * Describes the process_attempt return value.
1413 * @return external_single_structure
1416 public static function process_attempt_returns() {
1417 return new external_single_structure(
1419 'state' => new external_value(PARAM_ALPHANUMEXT
, 'state: the new attempt state:
1420 inprogress, finished, overdue, abandoned'),
1421 'warnings' => new external_warnings(),
1427 * Validate an attempt finished for review. The attempt would be reviewed by a user or a teacher.
1429 * @param array $params Array of parameters including the attemptid
1430 * @return array containing the attempt object and display options
1433 protected static function validate_attempt_review($params) {
1435 $attemptobj = quiz_attempt
::create($params['attemptid']);
1436 $attemptobj->check_review_capability();
1438 $displayoptions = $attemptobj->get_display_options(true);
1439 if ($attemptobj->is_own_attempt()) {
1440 if (!$attemptobj->is_finished()) {
1441 throw new moodle_exception('attemptclosed', 'quiz', $attemptobj->view_url());
1442 } else if (!$displayoptions->attempt
) {
1443 throw new moodle_exception('noreview', 'quiz', $attemptobj->view_url(), null,
1444 $attemptobj->cannot_review_message());
1446 } else if (!$attemptobj->is_review_allowed()) {
1447 throw new moodle_exception('noreviewattempt', 'quiz', $attemptobj->view_url());
1449 return [$attemptobj, $displayoptions];
1453 * Describes the parameters for get_attempt_review.
1455 * @return external_function_parameters
1458 public static function get_attempt_review_parameters() {
1459 return new external_function_parameters (
1461 'attemptid' => new external_value(PARAM_INT
, 'attempt id'),
1462 'page' => new external_value(PARAM_INT
, 'page number, empty for all the questions in all the pages',
1469 * Returns review information for the given finished attempt, can be used by users or teachers.
1471 * @param int $attemptid attempt id
1472 * @param int $page page number, empty for all the questions in all the pages
1473 * @return array of warnings and the attempt data, feedback and questions
1476 public static function get_attempt_review($attemptid, $page = -1) {
1481 'attemptid' => $attemptid,
1484 $params = self
::validate_parameters(self
::get_attempt_review_parameters(), $params);
1486 [$attemptobj, $displayoptions] = self
::validate_attempt_review($params);
1488 if ($params['page'] !== -1) {
1489 $page = $attemptobj->force_page_number_into_range($params['page']);
1494 // Make sure all users associated to the attempt steps are loaded. Otherwise, this will
1495 // trigger a debugging message.
1496 $attemptobj->preload_all_attempt_step_users();
1498 // Prepare the output.
1500 $result['attempt'] = $attemptobj->get_attempt();
1501 $result['questions'] = self
::get_attempt_questions_data($attemptobj, true, $page, true);
1503 $result['additionaldata'] = [];
1504 // Summary data (from behaviours).
1505 $summarydata = $attemptobj->get_additional_summary_data($displayoptions);
1506 foreach ($summarydata as $key => $data) {
1507 // This text does not need formatting (no need for external_format_[string|text]).
1508 $result['additionaldata'][] = [
1510 'title' => $data['title'], $attemptobj->get_quizobj()->get_context()->id
,
1511 'content' => $data['content'],
1515 // Feedback if there is any, and the user is allowed to see it now.
1516 $grade = quiz_rescale_grade($attemptobj->get_attempt()->sumgrades
, $attemptobj->get_quiz(), false);
1518 $feedback = $attemptobj->get_overall_feedback($grade);
1519 if ($displayoptions->overallfeedback
&& $feedback) {
1520 $result['additionaldata'][] = [
1522 'title' => get_string('feedback', 'quiz'),
1523 'content' => $feedback,
1527 if (!has_capability('mod/quiz:viewreports', $attemptobj->get_context()) &&
1528 ($displayoptions->marks
< question_display_options
::MARK_AND_MAX ||
1529 $attemptobj->get_attempt()->state
!= quiz_attempt
::FINISHED
)) {
1530 // Blank the mark if the teacher does not allow it.
1531 $result['attempt']->sumgrades
= null;
1533 $result['attempt']->gradeitemmarks
= [];
1534 foreach ($attemptobj->get_grade_item_totals() as $gradeitem) {
1535 $result['attempt']->gradeitemmarks
[] = [
1536 'name' => \core_external\util
::format_string($gradeitem->name
, $attemptobj->get_context()),
1537 'grade' => $gradeitem->grade
,
1538 'maxgrade' => $gradeitem->maxgrade
,
1543 $result['grade'] = $grade;
1544 $result['warnings'] = $warnings;
1549 * Describes the get_attempt_review return value.
1551 * @return external_single_structure
1554 public static function get_attempt_review_returns() {
1555 return new external_single_structure(
1557 'grade' => new external_value(PARAM_RAW
, 'grade for the quiz (or empty or "notyetgraded")'),
1558 'attempt' => self
::attempt_structure(),
1559 'additionaldata' => new external_multiple_structure(
1560 new external_single_structure(
1562 'id' => new external_value(PARAM_ALPHANUMEXT
, 'id of the data'),
1563 'title' => new external_value(PARAM_TEXT
, 'data title'),
1564 'content' => new external_value(PARAM_RAW
, 'data content'),
1568 'questions' => new external_multiple_structure(self
::question_structure()),
1569 'warnings' => new external_warnings(),
1575 * Describes the parameters for view_attempt.
1577 * @return external_function_parameters
1580 public static function view_attempt_parameters() {
1581 return new external_function_parameters (
1583 'attemptid' => new external_value(PARAM_INT
, 'attempt id'),
1584 'page' => new external_value(PARAM_INT
, 'page number'),
1585 'preflightdata' => new external_multiple_structure(
1586 new external_single_structure(
1588 'name' => new external_value(PARAM_ALPHANUMEXT
, 'data name'),
1589 'value' => new external_value(PARAM_RAW
, 'data value'),
1591 ), 'Preflight required data (like passwords)', VALUE_DEFAULT
, []
1598 * Trigger the attempt viewed event.
1600 * @param int $attemptid attempt id
1601 * @param int $page page number
1602 * @param array $preflightdata preflight required data (like passwords)
1603 * @return array of warnings and status result
1606 public static function view_attempt($attemptid, $page, $preflightdata = []) {
1611 'attemptid' => $attemptid,
1613 'preflightdata' => $preflightdata,
1615 $params = self
::validate_parameters(self
::view_attempt_parameters(), $params);
1616 list($attemptobj, $messages) = self
::validate_attempt($params);
1619 $attemptobj->fire_attempt_viewed_event();
1621 // Update attempt page, throwing an exception if $page is not valid.
1622 if (!$attemptobj->set_currentpage($params['page'])) {
1623 throw new moodle_exception('Out of sequence access', 'quiz', $attemptobj->view_url());
1627 $result['status'] = true;
1628 $result['warnings'] = $warnings;
1633 * Describes the view_attempt return value.
1635 * @return external_single_structure
1638 public static function view_attempt_returns() {
1639 return new external_single_structure(
1641 'status' => new external_value(PARAM_BOOL
, 'status: true if success'),
1642 'warnings' => new external_warnings(),
1648 * Describes the parameters for view_attempt_summary.
1650 * @return external_function_parameters
1653 public static function view_attempt_summary_parameters() {
1654 return new external_function_parameters (
1656 'attemptid' => new external_value(PARAM_INT
, 'attempt id'),
1657 'preflightdata' => new external_multiple_structure(
1658 new external_single_structure(
1660 'name' => new external_value(PARAM_ALPHANUMEXT
, 'data name'),
1661 'value' => new external_value(PARAM_RAW
, 'data value'),
1663 ), 'Preflight required data (like passwords)', VALUE_DEFAULT
, []
1670 * Trigger the attempt summary viewed event.
1672 * @param int $attemptid attempt id
1673 * @param array $preflightdata preflight required data (like passwords)
1674 * @return array of warnings and status result
1677 public static function view_attempt_summary($attemptid, $preflightdata = []) {
1682 'attemptid' => $attemptid,
1683 'preflightdata' => $preflightdata,
1685 $params = self
::validate_parameters(self
::view_attempt_summary_parameters(), $params);
1686 list($attemptobj, $messages) = self
::validate_attempt($params);
1689 $attemptobj->fire_attempt_summary_viewed_event();
1692 $result['status'] = true;
1693 $result['warnings'] = $warnings;
1698 * Describes the view_attempt_summary return value.
1700 * @return external_single_structure
1703 public static function view_attempt_summary_returns() {
1704 return new external_single_structure(
1706 'status' => new external_value(PARAM_BOOL
, 'status: true if success'),
1707 'warnings' => new external_warnings(),
1713 * Describes the parameters for view_attempt_review.
1715 * @return external_function_parameters
1718 public static function view_attempt_review_parameters() {
1719 return new external_function_parameters (
1721 'attemptid' => new external_value(PARAM_INT
, 'attempt id'),
1727 * Trigger the attempt reviewed event.
1729 * @param int $attemptid attempt id
1730 * @return array of warnings and status result
1733 public static function view_attempt_review($attemptid) {
1738 'attemptid' => $attemptid,
1740 $params = self
::validate_parameters(self
::view_attempt_review_parameters(), $params);
1741 list($attemptobj, $displayoptions) = self
::validate_attempt_review($params);
1744 $attemptobj->fire_attempt_reviewed_event();
1747 $result['status'] = true;
1748 $result['warnings'] = $warnings;
1753 * Describes the view_attempt_review return value.
1755 * @return external_single_structure
1758 public static function view_attempt_review_returns() {
1759 return new external_single_structure(
1761 'status' => new external_value(PARAM_BOOL
, 'status: true if success'),
1762 'warnings' => new external_warnings(),
1768 * Describes the parameters for view_quiz.
1770 * @return external_function_parameters
1773 public static function get_quiz_feedback_for_grade_parameters() {
1774 return new external_function_parameters (
1776 'quizid' => new external_value(PARAM_INT
, 'quiz instance id'),
1777 'grade' => new external_value(PARAM_FLOAT
, 'the grade to check'),
1783 * Get the feedback text that should be show to a student who got the given grade in the given quiz.
1785 * @param int $quizid quiz instance id
1786 * @param float $grade the grade to check
1787 * @return array of warnings and status result
1790 public static function get_quiz_feedback_for_grade($quizid, $grade) {
1794 'quizid' => $quizid,
1797 $params = self
::validate_parameters(self
::get_quiz_feedback_for_grade_parameters(), $params);
1800 list($quiz, $course, $cm, $context) = self
::validate_quiz($params['quizid']);
1803 $result['feedbacktext'] = '';
1804 $result['feedbacktextformat'] = FORMAT_MOODLE
;
1806 $feedback = quiz_feedback_record_for_grade($params['grade'], $quiz);
1807 if (!empty($feedback->feedbacktext
)) {
1808 list($text, $format) = \core_external\util
::format_text(
1809 $feedback->feedbacktext
,
1810 $feedback->feedbacktextformat
,
1816 $result['feedbacktext'] = $text;
1817 $result['feedbacktextformat'] = $format;
1818 $feedbackinlinefiles = util
::get_area_files($context->id
, 'mod_quiz', 'feedback', $feedback->id
);
1819 if (!empty($feedbackinlinefiles)) {
1820 $result['feedbackinlinefiles'] = $feedbackinlinefiles;
1824 $result['warnings'] = $warnings;
1829 * Describes the get_quiz_feedback_for_grade return value.
1831 * @return external_single_structure
1834 public static function get_quiz_feedback_for_grade_returns() {
1835 return new external_single_structure(
1837 'feedbacktext' => new external_value(PARAM_RAW
, 'the comment that corresponds to this grade (empty for none)'),
1838 'feedbacktextformat' => new external_format_value('feedbacktext', VALUE_OPTIONAL
),
1839 'feedbackinlinefiles' => new external_files('feedback inline files', VALUE_OPTIONAL
),
1840 'warnings' => new external_warnings(),
1846 * Describes the parameters for get_quiz_access_information.
1848 * @return external_function_parameters
1851 public static function get_quiz_access_information_parameters() {
1852 return new external_function_parameters (
1854 'quizid' => new external_value(PARAM_INT
, 'quiz instance id')
1860 * Return access information for a given quiz.
1862 * @param int $quizid quiz instance id
1863 * @return array of warnings and the access information
1866 public static function get_quiz_access_information($quizid) {
1874 $params = self
::validate_parameters(self
::get_quiz_access_information_parameters(), $params);
1876 list($quiz, $course, $cm, $context) = self
::validate_quiz($params['quizid']);
1879 // Capabilities first.
1880 $result['canattempt'] = has_capability('mod/quiz:attempt', $context);;
1881 $result['canmanage'] = has_capability('mod/quiz:manage', $context);;
1882 $result['canpreview'] = has_capability('mod/quiz:preview', $context);;
1883 $result['canreviewmyattempts'] = has_capability('mod/quiz:reviewmyattempts', $context);;
1884 $result['canviewreports'] = has_capability('mod/quiz:viewreports', $context);;
1886 // Access manager now.
1887 $quizobj = quiz_settings
::create($cm->instance
, $USER->id
);
1888 $ignoretimelimits = has_capability('mod/quiz:ignoretimelimits', $context, null, false);
1890 $accessmanager = new access_manager($quizobj, $timenow, $ignoretimelimits);
1892 $result['accessrules'] = $accessmanager->describe_rules();
1893 $result['activerulenames'] = $accessmanager->get_active_rule_names();
1894 $result['preventaccessreasons'] = $accessmanager->prevent_access();
1896 $result['warnings'] = $warnings;
1901 * Describes the get_quiz_access_information return value.
1903 * @return external_single_structure
1906 public static function get_quiz_access_information_returns() {
1907 return new external_single_structure(
1909 'canattempt' => new external_value(PARAM_BOOL
, 'Whether the user can do the quiz or not.'),
1910 'canmanage' => new external_value(PARAM_BOOL
, 'Whether the user can edit the quiz settings or not.'),
1911 'canpreview' => new external_value(PARAM_BOOL
, 'Whether the user can preview the quiz or not.'),
1912 'canreviewmyattempts' => new external_value(PARAM_BOOL
, 'Whether the users can review their previous attempts
1914 'canviewreports' => new external_value(PARAM_BOOL
, 'Whether the user can view the quiz reports or not.'),
1915 'accessrules' => new external_multiple_structure(
1916 new external_value(PARAM_TEXT
, 'rule description'), 'list of rules'),
1917 'activerulenames' => new external_multiple_structure(
1918 new external_value(PARAM_PLUGIN
, 'rule plugin names'), 'list of active rules'),
1919 'preventaccessreasons' => new external_multiple_structure(
1920 new external_value(PARAM_TEXT
, 'access restriction description'), 'list of reasons'),
1921 'warnings' => new external_warnings(),
1927 * Describes the parameters for get_attempt_access_information.
1929 * @return external_function_parameters
1932 public static function get_attempt_access_information_parameters() {
1933 return new external_function_parameters (
1935 'quizid' => new external_value(PARAM_INT
, 'quiz instance id'),
1936 'attemptid' => new external_value(PARAM_INT
, 'attempt id, 0 for the user last attempt if exists', VALUE_DEFAULT
, 0),
1942 * Return access information for a given attempt in a quiz.
1944 * @param int $quizid quiz instance id
1945 * @param int $attemptid attempt id, 0 for the user last attempt if exists
1946 * @return array of warnings and the access information
1949 public static function get_attempt_access_information($quizid, $attemptid = 0) {
1955 'quizid' => $quizid,
1956 'attemptid' => $attemptid,
1958 $params = self
::validate_parameters(self
::get_attempt_access_information_parameters(), $params);
1960 list($quiz, $course, $cm, $context) = self
::validate_quiz($params['quizid']);
1962 $attempttocheck = null;
1963 if (!empty($params['attemptid'])) {
1964 $attemptobj = quiz_attempt
::create($params['attemptid']);
1965 if ($attemptobj->get_userid() != $USER->id
) {
1966 throw new moodle_exception('notyourattempt', 'quiz', $attemptobj->view_url());
1968 $attempttocheck = $attemptobj->get_attempt();
1971 // Access manager now.
1972 $quizobj = quiz_settings
::create($cm->instance
, $USER->id
);
1973 $ignoretimelimits = has_capability('mod/quiz:ignoretimelimits', $context, null, false);
1975 $accessmanager = new access_manager($quizobj, $timenow, $ignoretimelimits);
1977 $attempts = quiz_get_user_attempts($quiz->id
, $USER->id
, 'finished', true);
1978 $lastfinishedattempt = end($attempts);
1979 if ($unfinishedattempt = quiz_get_user_attempt_unfinished($quiz->id
, $USER->id
)) {
1980 $attempts[] = $unfinishedattempt;
1982 // Check if the attempt is now overdue. In that case the state will change.
1983 $quizobj->create_attempt_object($unfinishedattempt)->handle_if_time_expired(time(), false);
1985 if ($unfinishedattempt->state
!= quiz_attempt
::IN_PROGRESS
and $unfinishedattempt->state
!= quiz_attempt
::OVERDUE
) {
1986 $lastfinishedattempt = $unfinishedattempt;
1989 $numattempts = count($attempts);
1991 if (!$attempttocheck) {
1992 $attempttocheck = $unfinishedattempt ?
: $lastfinishedattempt;
1996 $result['isfinished'] = $accessmanager->is_finished($numattempts, $lastfinishedattempt);
1997 $result['preventnewattemptreasons'] = $accessmanager->prevent_new_attempt($numattempts, $lastfinishedattempt);
1999 if ($attempttocheck) {
2000 $endtime = $accessmanager->get_end_time($attempttocheck);
2001 $result['endtime'] = ($endtime === false) ?
0 : $endtime;
2002 $attemptid = $unfinishedattempt ?
$unfinishedattempt->id
: null;
2003 $result['ispreflightcheckrequired'] = $accessmanager->is_preflight_check_required($attemptid);
2006 $result['warnings'] = $warnings;
2011 * Describes the get_attempt_access_information return value.
2013 * @return external_single_structure
2016 public static function get_attempt_access_information_returns() {
2017 return new external_single_structure(
2019 'endtime' => new external_value(PARAM_INT
, 'When the attempt must be submitted (determined by rules).',
2021 'isfinished' => new external_value(PARAM_BOOL
, 'Whether there is no way the user will ever be allowed to attempt.'),
2022 'ispreflightcheckrequired' => new external_value(PARAM_BOOL
, 'whether a check is required before the user
2023 starts/continues his attempt.', VALUE_OPTIONAL
),
2024 'preventnewattemptreasons' => new external_multiple_structure(
2025 new external_value(PARAM_TEXT
, 'access restriction description'),
2027 'warnings' => new external_warnings(),
2033 * Describes the parameters for get_quiz_required_qtypes.
2035 * @return external_function_parameters
2038 public static function get_quiz_required_qtypes_parameters() {
2039 return new external_function_parameters (
2041 'quizid' => new external_value(PARAM_INT
, 'quiz instance id')
2047 * Return the potential question types that would be required for a given quiz.
2048 * Please note that for random question types we return the potential question types in the category choosen.
2050 * @param int $quizid quiz instance id
2051 * @return array of warnings and the access information
2054 public static function get_quiz_required_qtypes($quizid) {
2062 $params = self
::validate_parameters(self
::get_quiz_required_qtypes_parameters(), $params);
2064 list($quiz, $course, $cm, $context) = self
::validate_quiz($params['quizid']);
2066 $quizobj = quiz_settings
::create($cm->instance
, $USER->id
);
2067 $quizobj->preload_questions();
2069 // Question types used.
2071 $result['questiontypes'] = $quizobj->get_all_question_types_used(true);
2072 $result['warnings'] = $warnings;
2077 * Describes the get_quiz_required_qtypes return value.
2079 * @return external_single_structure
2082 public static function get_quiz_required_qtypes_returns() {
2083 return new external_single_structure(
2085 'questiontypes' => new external_multiple_structure(
2086 new external_value(PARAM_PLUGIN
, 'question type'), 'list of question types used in the quiz'),
2087 'warnings' => new external_warnings(),