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