Merge branch 'MDL-80087_main' of https://github.com/PhilippImhof/moodle
[moodle.git] / mod / quiz / classes / external.php
blob685d409632a2abb0bee257411207d317193543a9
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17 /**
18 * Quiz external API
20 * @package mod_quiz
21 * @category external
22 * @copyright 2016 Juan Leyva <juan@moodle.com>
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24 * @since Moodle 3.1
27 use core_course\external\helper_for_get_mods_by_courses;
28 use core_external\external_api;
29 use core_external\external_files;
30 use core_external\external_format_value;
31 use core_external\external_function_parameters;
32 use core_external\external_multiple_structure;
33 use core_external\external_single_structure;
34 use core_external\external_value;
35 use core_external\external_warnings;
36 use core_external\util;
37 use mod_quiz\access_manager;
38 use mod_quiz\quiz_attempt;
39 use mod_quiz\quiz_settings;
41 defined('MOODLE_INTERNAL') || die;
43 require_once($CFG->dirroot . '/mod/quiz/locallib.php');
45 /**
46 * Quiz external functions
48 * @package mod_quiz
49 * @category external
50 * @copyright 2016 Juan Leyva <juan@moodle.com>
51 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
52 * @since Moodle 3.1
54 class mod_quiz_external extends external_api {
56 /**
57 * Describes the parameters for get_quizzes_by_courses.
59 * @return external_function_parameters
60 * @since Moodle 3.1
62 public static function get_quizzes_by_courses_parameters() {
63 return new external_function_parameters (
65 'courseids' => new external_multiple_structure(
66 new external_value(PARAM_INT, 'course id'), 'Array of course ids', VALUE_DEFAULT, []
72 /**
73 * Returns a list of quizzes in a provided list of courses,
74 * if no list is provided all quizzes that the user can view will be returned.
76 * @param array $courseids Array of course ids
77 * @return array of quizzes details
78 * @since Moodle 3.1
80 public static function get_quizzes_by_courses($courseids = []) {
81 global $USER;
83 $warnings = [];
84 $returnedquizzes = [];
86 $params = [
87 'courseids' => $courseids,
89 $params = self::validate_parameters(self::get_quizzes_by_courses_parameters(), $params);
91 $mycourses = [];
92 if (empty($params['courseids'])) {
93 $mycourses = enrol_get_my_courses();
94 $params['courseids'] = array_keys($mycourses);
97 // Ensure there are courseids to loop through.
98 if (!empty($params['courseids'])) {
100 list($courses, $warnings) = util::validate_courses($params['courseids'], $mycourses);
102 // Get the quizzes in this course, this function checks users visibility permissions.
103 // We can avoid then additional validate_context calls.
104 $quizzes = get_all_instances_in_courses("quiz", $courses);
105 foreach ($quizzes as $quiz) {
106 $context = context_module::instance($quiz->coursemodule);
108 // Update quiz with override information.
109 $quiz = quiz_update_effective_access($quiz, $USER->id);
111 // Entry to return.
112 $quizdetails = helper_for_get_mods_by_courses::standard_coursemodule_element_values(
113 $quiz, 'mod_quiz', 'mod/quiz:view', 'mod/quiz:view');
115 if (has_capability('mod/quiz:view', $context)) {
116 $quizdetails['introfiles'] = util::get_area_files($context->id, 'mod_quiz', 'intro', false, false);
117 $viewablefields = ['timeopen', 'timeclose', 'attempts', 'timelimit', 'grademethod', 'decimalpoints',
118 'questiondecimalpoints', 'sumgrades', 'grade', 'preferredbehaviour'];
120 // Sometimes this function returns just empty.
121 $hasfeedback = quiz_has_feedback($quiz);
122 $quizdetails['hasfeedback'] = (!empty($hasfeedback)) ? 1 : 0;
124 $timenow = time();
125 $quizobj = quiz_settings::create($quiz->id, $USER->id);
126 $accessmanager = new access_manager($quizobj, $timenow, has_capability('mod/quiz:ignoretimelimits',
127 $context, null, false));
129 // Fields the user could see if have access to the quiz.
130 if (!$accessmanager->prevent_access()) {
131 $quizdetails['hasquestions'] = (int) $quizobj->has_questions();
132 $quizdetails['autosaveperiod'] = get_config('quiz', 'autosaveperiod');
134 $additionalfields = ['attemptonlast', 'reviewattempt', 'reviewcorrectness', 'reviewmaxmarks', 'reviewmarks',
135 'reviewspecificfeedback', 'reviewgeneralfeedback', 'reviewrightanswer',
136 'reviewoverallfeedback', 'questionsperpage', 'navmethod',
137 'browsersecurity', 'delay1', 'delay2', 'showuserpicture', 'showblocks',
138 'completionattemptsexhausted', 'overduehandling',
139 'graceperiod', 'canredoquestions', 'allowofflineattempts'];
140 $viewablefields = array_merge($viewablefields, $additionalfields);
142 // Any course module fields that previously existed in quiz.
143 $quizdetails['completionpass'] = $quizobj->get_cm()->completionpassgrade;
146 // Fields only for managers.
147 if (has_capability('moodle/course:manageactivities', $context)) {
148 $additionalfields = ['shuffleanswers', 'timecreated', 'timemodified', 'password', 'subnet'];
149 $viewablefields = array_merge($viewablefields, $additionalfields);
152 foreach ($viewablefields as $field) {
153 $quizdetails[$field] = $quiz->{$field};
156 $returnedquizzes[] = $quizdetails;
159 $result = [];
160 $result['quizzes'] = $returnedquizzes;
161 $result['warnings'] = $warnings;
162 return $result;
166 * Describes the get_quizzes_by_courses return value.
168 * @return external_single_structure
169 * @since Moodle 3.1
171 public static function get_quizzes_by_courses_returns() {
172 return new external_single_structure(
174 'quizzes' => new external_multiple_structure(
175 new external_single_structure(array_merge(
176 helper_for_get_mods_by_courses::standard_coursemodule_elements_returns(true),
178 'timeopen' => new external_value(PARAM_INT, 'The time when this quiz opens. (0 = no restriction.)',
179 VALUE_OPTIONAL),
180 'timeclose' => new external_value(PARAM_INT, 'The time when this quiz closes. (0 = no restriction.)',
181 VALUE_OPTIONAL),
182 'timelimit' => new external_value(PARAM_INT, 'The time limit for quiz attempts, in seconds.',
183 VALUE_OPTIONAL),
184 'overduehandling' => new external_value(PARAM_ALPHA, 'The method used to handle overdue attempts.
185 \'autosubmit\', \'graceperiod\' or \'autoabandon\'.',
186 VALUE_OPTIONAL),
187 'graceperiod' => new external_value(PARAM_INT, 'The amount of time (in seconds) after the time limit
188 runs out during which attempts can still be submitted,
189 if overduehandling is set to allow it.', VALUE_OPTIONAL),
190 'preferredbehaviour' => new external_value(PARAM_ALPHANUMEXT, 'The behaviour to ask questions to use.',
191 VALUE_OPTIONAL),
192 'canredoquestions' => new external_value(PARAM_INT, 'Allows students to redo any completed question
193 within a quiz attempt.', VALUE_OPTIONAL),
194 'attempts' => new external_value(PARAM_INT, 'The maximum number of attempts a student is allowed.',
195 VALUE_OPTIONAL),
196 'attemptonlast' => new external_value(PARAM_INT, 'Whether subsequent attempts start from the answer
197 to the previous attempt (1) or start blank (0).',
198 VALUE_OPTIONAL),
199 'grademethod' => new external_value(PARAM_INT, 'One of the values QUIZ_GRADEHIGHEST, QUIZ_GRADEAVERAGE,
200 QUIZ_ATTEMPTFIRST or QUIZ_ATTEMPTLAST.', VALUE_OPTIONAL),
201 'decimalpoints' => new external_value(PARAM_INT, 'Number of decimal points to use when displaying
202 grades.', VALUE_OPTIONAL),
203 'questiondecimalpoints' => new external_value(PARAM_INT, 'Number of decimal points to use when
204 displaying question grades.
205 (-1 means use decimalpoints.)', VALUE_OPTIONAL),
206 'reviewattempt' => new external_value(PARAM_INT, 'Whether users are allowed to review their quiz
207 attempts at various times. This is a bit field, decoded by the
208 \mod_quiz\question\display_options class. It is formed by ORing
209 together the constants defined there.', VALUE_OPTIONAL),
210 'reviewcorrectness' => new external_value(PARAM_INT, 'Whether users are allowed to review their quiz
211 attempts at various times.A bit field, like reviewattempt.', VALUE_OPTIONAL),
212 'reviewmaxmarks' => new external_value(PARAM_INT, 'Whether users are allowed to review their quiz
213 attempts at various times. A bit field, like reviewattempt.', VALUE_OPTIONAL),
214 'reviewmarks' => new external_value(PARAM_INT, 'Whether users are allowed to review their quiz attempts
215 at various times. A bit field, like reviewattempt.',
216 VALUE_OPTIONAL),
217 'reviewspecificfeedback' => new external_value(PARAM_INT, 'Whether users are allowed to review their
218 quiz attempts at various times. A bit field, like
219 reviewattempt.', VALUE_OPTIONAL),
220 'reviewgeneralfeedback' => new external_value(PARAM_INT, 'Whether users are allowed to review their
221 quiz attempts at various times. A bit field, like
222 reviewattempt.', VALUE_OPTIONAL),
223 'reviewrightanswer' => new external_value(PARAM_INT, 'Whether users are allowed to review their quiz
224 attempts at various times. A bit field, like
225 reviewattempt.', VALUE_OPTIONAL),
226 'reviewoverallfeedback' => new external_value(PARAM_INT, 'Whether users are allowed to review their quiz
227 attempts at various times. A bit field, like
228 reviewattempt.', VALUE_OPTIONAL),
229 'questionsperpage' => new external_value(PARAM_INT, 'How often to insert a page break when editing
230 the quiz, or when shuffling the question order.',
231 VALUE_OPTIONAL),
232 'navmethod' => new external_value(PARAM_ALPHA, 'Any constraints on how the user is allowed to navigate
233 around the quiz. Currently recognised values are
234 \'free\' and \'seq\'.', VALUE_OPTIONAL),
235 'shuffleanswers' => new external_value(PARAM_INT, 'Whether the parts of the question should be shuffled,
236 in those question types that support it.', VALUE_OPTIONAL),
237 'sumgrades' => new external_value(PARAM_FLOAT, 'The total of all the question instance maxmarks.',
238 VALUE_OPTIONAL),
239 'grade' => new external_value(PARAM_FLOAT, 'The total that the quiz overall grade is scaled to be
240 out of.', VALUE_OPTIONAL),
241 'timecreated' => new external_value(PARAM_INT, 'The time when the quiz was added to the course.',
242 VALUE_OPTIONAL),
243 'timemodified' => new external_value(PARAM_INT, 'Last modified time.',
244 VALUE_OPTIONAL),
245 'password' => new external_value(PARAM_RAW, 'A password that the student must enter before starting or
246 continuing a quiz attempt.', VALUE_OPTIONAL),
247 'subnet' => new external_value(PARAM_RAW, 'Used to restrict the IP addresses from which this quiz can
248 be attempted. The format is as requried by the address_in_subnet
249 function.', VALUE_OPTIONAL),
250 'browsersecurity' => new external_value(PARAM_ALPHANUMEXT, 'Restriciton on the browser the student must
251 use. E.g. \'securewindow\'.', VALUE_OPTIONAL),
252 'delay1' => new external_value(PARAM_INT, 'Delay that must be left between the first and second attempt,
253 in seconds.', VALUE_OPTIONAL),
254 'delay2' => new external_value(PARAM_INT, 'Delay that must be left between the second and subsequent
255 attempt, in seconds.', VALUE_OPTIONAL),
256 'showuserpicture' => new external_value(PARAM_INT, 'Option to show the user\'s picture during the
257 attempt and on the review page.', VALUE_OPTIONAL),
258 'showblocks' => new external_value(PARAM_INT, 'Whether blocks should be shown on the attempt.php and
259 review.php pages.', VALUE_OPTIONAL),
260 'completionattemptsexhausted' => new external_value(PARAM_INT, 'Mark quiz complete when the student has
261 exhausted the maximum number of attempts',
262 VALUE_OPTIONAL),
263 'completionpass' => new external_value(PARAM_INT, 'Whether to require passing grade', VALUE_OPTIONAL),
264 'allowofflineattempts' => new external_value(PARAM_INT, 'Whether to allow the quiz to be attempted
265 offline in the mobile app', VALUE_OPTIONAL),
266 'autosaveperiod' => new external_value(PARAM_INT, 'Auto-save delay', VALUE_OPTIONAL),
267 'hasfeedback' => new external_value(PARAM_INT, 'Whether the quiz has any non-blank feedback text',
268 VALUE_OPTIONAL),
269 'hasquestions' => new external_value(PARAM_INT, 'Whether the quiz has questions', VALUE_OPTIONAL),
273 'warnings' => new external_warnings(),
280 * Utility function for validating a quiz.
282 * @param int $quizid quiz instance id
283 * @return array array containing the quiz, course, context and course module objects
284 * @since Moodle 3.1
286 protected static function validate_quiz($quizid) {
287 global $DB;
289 // Request and permission validation.
290 $quiz = $DB->get_record('quiz', ['id' => $quizid], '*', MUST_EXIST);
291 list($course, $cm) = get_course_and_cm_from_instance($quiz, 'quiz');
293 $context = context_module::instance($cm->id);
294 self::validate_context($context);
296 return [$quiz, $course, $cm, $context];
300 * Describes the parameters for view_quiz.
302 * @return external_function_parameters
303 * @since Moodle 3.1
305 public static function view_quiz_parameters() {
306 return new external_function_parameters (
308 'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
314 * Trigger the course module viewed event and update the module completion status.
316 * @param int $quizid quiz instance id
317 * @return array of warnings and status result
318 * @since Moodle 3.1
320 public static function view_quiz($quizid) {
321 global $DB;
323 $params = self::validate_parameters(self::view_quiz_parameters(), ['quizid' => $quizid]);
324 $warnings = [];
326 list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
328 // Trigger course_module_viewed event and completion.
329 quiz_view($quiz, $course, $cm, $context);
331 $result = [];
332 $result['status'] = true;
333 $result['warnings'] = $warnings;
334 return $result;
338 * Describes the view_quiz return value.
340 * @return external_single_structure
341 * @since Moodle 3.1
343 public static function view_quiz_returns() {
344 return new external_single_structure(
346 'status' => new external_value(PARAM_BOOL, 'status: true if success'),
347 'warnings' => new external_warnings(),
353 * Describes the parameters for get_user_attempts.
355 * @return external_function_parameters
356 * @since Moodle 3.1
358 public static function get_user_attempts_parameters() {
359 return new external_function_parameters (
361 'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
362 'userid' => new external_value(PARAM_INT, 'user id, empty for current user', VALUE_DEFAULT, 0),
363 'status' => new external_value(PARAM_ALPHA, 'quiz status: all, finished or unfinished', VALUE_DEFAULT, 'finished'),
364 'includepreviews' => new external_value(PARAM_BOOL, 'whether to include previews or not', VALUE_DEFAULT, false),
371 * Return a list of attempts for the given quiz and user.
373 * @param int $quizid quiz instance id
374 * @param int $userid user id
375 * @param string $status quiz status: all, finished or unfinished
376 * @param bool $includepreviews whether to include previews or not
377 * @return array of warnings and the list of attempts
378 * @since Moodle 3.1
380 public static function get_user_attempts($quizid, $userid = 0, $status = 'finished', $includepreviews = false) {
381 global $USER;
383 $warnings = [];
385 $params = [
386 'quizid' => $quizid,
387 'userid' => $userid,
388 'status' => $status,
389 'includepreviews' => $includepreviews,
391 $params = self::validate_parameters(self::get_user_attempts_parameters(), $params);
393 list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
395 if (!in_array($params['status'], ['all', 'finished', 'unfinished'])) {
396 throw new invalid_parameter_exception('Invalid status value');
399 // Default value for userid.
400 if (empty($params['userid'])) {
401 $params['userid'] = $USER->id;
404 $user = core_user::get_user($params['userid'], '*', MUST_EXIST);
405 core_user::require_active_user($user);
407 // Extra checks so only users with permissions can view other users attempts.
408 if ($USER->id != $user->id) {
409 require_capability('mod/quiz:viewreports', $context);
412 // Update quiz with override information.
413 $quiz = quiz_update_effective_access($quiz, $params['userid']);
414 $attempts = quiz_get_user_attempts($quiz->id, $user->id, $params['status'], $params['includepreviews']);
415 $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;
437 $result = [];
438 $result['attempts'] = $attemptresponse;
439 $result['warnings'] = $warnings;
440 return $result;
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.',
453 VALUE_OPTIONAL),
454 'userid' => new external_value(PARAM_INT, 'Foreign key reference to the user whose attempt this is.',
455 VALUE_OPTIONAL),
456 'attempt' => new external_value(PARAM_INT, 'Sequentially numbers this students attempts at this quiz.',
457 VALUE_OPTIONAL),
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
491 * @since Moodle 3.1
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
506 * @since Moodle 3.1
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
523 * @since Moodle 3.1
525 public static function get_user_best_grade($quizid, $userid = 0) {
526 global $DB, $USER, $CFG;
527 require_once($CFG->libdir . '/gradelib.php');
529 $warnings = [];
531 $params = [
532 'quizid' => $quizid,
533 'userid' => $userid,
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);
552 $result = [];
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;
558 if ($attempts) {
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;
562 } else {
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;
573 } else {
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;
589 return $result;
593 * Describes the get_user_best_grade return value.
595 * @return external_single_structure
596 * @since Moodle 3.1
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
613 * @since Moodle 3.1
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
631 * @since Moodle 3.1
633 public static function get_combined_review_options($quizid, $userid = 0) {
634 global $DB, $USER;
636 $warnings = [];
638 $params = [
639 'quizid' => $quizid,
640 'userid' => $userid,
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);
663 $result = [];
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][] = [
672 "name" => $key,
673 "value" => (!empty($value)) ? $value : 0
678 $result['warnings'] = $warnings;
679 return $result;
683 * Describes the get_combined_review_options return value.
685 * @return external_single_structure
686 * @since Moodle 3.1
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
716 * @since Moodle 3.1
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
743 * @since Moodle 3.1
745 public static function start_attempt($quizid, $preflightdata = [], $forcenew = false) {
746 global $DB, $USER;
748 $warnings = [];
749 $attempt = [];
751 $params = [
752 'quizid' => $quizid,
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);
763 // Check questions.
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.
769 $timenow = time();
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);
776 // Check access.
777 if (!$quizobj->is_preview_user() && $messages) {
778 // Create warnings with the exact messages.
779 foreach ($messages as $message) {
780 $warnings[] = [
781 'item' => 'quiz',
782 'itemid' => $quiz->id,
783 'warningcode' => '1',
784 'message' => clean_text($message, PARAM_TEXT)
787 } else {
788 if ($accessmanager->is_preflight_check_required($currentattemptid)) {
789 // Need to do some checks before allowing the user to continue.
791 $provideddata = [];
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());
809 } else {
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);
817 $result = [];
818 $result['attempt'] = $attempt;
819 $result['warnings'] = $warnings;
820 return $result;
824 * Describes the start_attempt return value.
826 * @return external_single_structure
827 * @since Moodle 3.1
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
845 * @since Moodle 3.1
847 protected static function validate_attempt($params, $checkaccessrules = true, $failifoverdue = true) {
848 global $USER;
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());
868 $messages = [];
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());
879 // Attempt closed?.
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())) {
888 $provideddata = [];
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());
912 // Check slots.
913 $slots = $attemptobj->get_slots($params['page']);
915 if (empty($slots)) {
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.
927 * @since Moodle 3.1
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',
951 VALUE_OPTIONAL),
952 'hasautosavedstep' => new external_value(PARAM_BOOL, 'whether this question attempt has autosaved data',
953 VALUE_OPTIONAL),
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.',
957 VALUE_OPTIONAL),
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',
963 VALUE_OPTIONAL),
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') {
983 global $PAGE;
985 $questions = [];
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.
999 $question = [
1000 'slot' => $slot,
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);
1032 if (!$haveaccess) {
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;
1068 return $questions;
1072 * Describes the parameters for get_attempt_data.
1074 * @return external_function_parameters
1075 * @since Moodle 3.1
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
1101 * @since Moodle 3.1
1103 public static function get_attempt_data($attemptid, $page, $preflightdata = []) {
1104 global $PAGE;
1106 $warnings = [];
1108 $params = [
1109 'attemptid' => $attemptid,
1110 'page' => $page,
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'])) {
1118 $nextpage = -1;
1119 } else {
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');
1127 $result = [];
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']);
1134 return $result;
1138 * Describes the get_attempt_data return value.
1140 * @return external_single_structure
1141 * @since Moodle 3.1
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
1162 * @since Moodle 3.1
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
1186 * @since Moodle 3.1
1188 public static function get_attempt_summary($attemptid, $preflightdata = []) {
1190 $warnings = [];
1192 $params = [
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);
1200 $result = [];
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();
1210 return $result;
1214 * Describes the get_attempt_summary return value.
1216 * @return external_single_structure
1217 * @since Moodle 3.1
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
1233 * @since Moodle 3.1
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
1266 * @since Moodle 3.1
1268 public static function save_attempt($attemptid, $data, $preflightdata = []) {
1269 global $DB, $USER;
1271 $warnings = [];
1273 $params = [
1274 'attemptid' => $attemptid,
1275 'data' => $data,
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.
1289 $_POST = [];
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'];
1295 $timenow = time();
1296 // Update the timemodifiedoffline field.
1297 $attemptobj->set_offline_modified_time($timenow);
1298 $attemptobj->process_auto_save($timenow);
1299 $transaction->allow_commit();
1301 $result = [];
1302 $result['status'] = true;
1303 $result['warnings'] = $warnings;
1304 return $result;
1308 * Describes the save_attempt return value.
1310 * @return external_single_structure
1311 * @since Moodle 3.1
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
1326 * @since Moodle 3.1
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
1365 * @since Moodle 3.1
1367 public static function process_attempt($attemptid, $data, $finishattempt = false, $timeup = false, $preflightdata = []) {
1368 global $USER;
1370 $warnings = [];
1372 $params = [
1373 'attemptid' => $attemptid,
1374 'data' => $data,
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.
1392 $_POST = [];
1393 foreach ($params['data'] as $element) {
1394 $_POST[$element['name']] = $element['value'];
1395 $_REQUEST[$element['name']] = $element['value'];
1397 $timenow = time();
1398 $finishattempt = $params['finishattempt'];
1399 $timeup = $params['timeup'];
1401 $result = [];
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;
1407 return $result;
1411 * Describes the process_attempt return value.
1413 * @return external_single_structure
1414 * @since Moodle 3.1
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
1431 * @since Moodle 3.1
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
1456 * @since Moodle 3.1
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',
1463 VALUE_DEFAULT, -1),
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
1474 * @since Moodle 3.1
1476 public static function get_attempt_review($attemptid, $page = -1) {
1478 $warnings = [];
1480 $params = [
1481 'attemptid' => $attemptid,
1482 'page' => $page,
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']);
1490 } else {
1491 $page = 'all';
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.
1499 $result = [];
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'][] = [
1509 'id' => $key,
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'][] = [
1521 'id' => 'feedback',
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;
1532 } else {
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;
1545 return $result;
1549 * Describes the get_attempt_review return value.
1551 * @return external_single_structure
1552 * @since Moodle 3.1
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
1578 * @since Moodle 3.1
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
1604 * @since Moodle 3.1
1606 public static function view_attempt($attemptid, $page, $preflightdata = []) {
1608 $warnings = [];
1610 $params = [
1611 'attemptid' => $attemptid,
1612 'page' => $page,
1613 'preflightdata' => $preflightdata,
1615 $params = self::validate_parameters(self::view_attempt_parameters(), $params);
1616 list($attemptobj, $messages) = self::validate_attempt($params);
1618 // Log action.
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());
1626 $result = [];
1627 $result['status'] = true;
1628 $result['warnings'] = $warnings;
1629 return $result;
1633 * Describes the view_attempt return value.
1635 * @return external_single_structure
1636 * @since Moodle 3.1
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
1651 * @since Moodle 3.1
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
1675 * @since Moodle 3.1
1677 public static function view_attempt_summary($attemptid, $preflightdata = []) {
1679 $warnings = [];
1681 $params = [
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);
1688 // Log action.
1689 $attemptobj->fire_attempt_summary_viewed_event();
1691 $result = [];
1692 $result['status'] = true;
1693 $result['warnings'] = $warnings;
1694 return $result;
1698 * Describes the view_attempt_summary return value.
1700 * @return external_single_structure
1701 * @since Moodle 3.1
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
1716 * @since Moodle 3.1
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
1731 * @since Moodle 3.1
1733 public static function view_attempt_review($attemptid) {
1735 $warnings = [];
1737 $params = [
1738 'attemptid' => $attemptid,
1740 $params = self::validate_parameters(self::view_attempt_review_parameters(), $params);
1741 list($attemptobj, $displayoptions) = self::validate_attempt_review($params);
1743 // Log action.
1744 $attemptobj->fire_attempt_reviewed_event();
1746 $result = [];
1747 $result['status'] = true;
1748 $result['warnings'] = $warnings;
1749 return $result;
1753 * Describes the view_attempt_review return value.
1755 * @return external_single_structure
1756 * @since Moodle 3.1
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
1771 * @since Moodle 3.1
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
1788 * @since Moodle 3.1
1790 public static function get_quiz_feedback_for_grade($quizid, $grade) {
1791 global $DB;
1793 $params = [
1794 'quizid' => $quizid,
1795 'grade' => $grade,
1797 $params = self::validate_parameters(self::get_quiz_feedback_for_grade_parameters(), $params);
1798 $warnings = [];
1800 list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
1802 $result = [];
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,
1811 $context,
1812 'mod_quiz',
1813 'feedback',
1814 $feedback->id
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;
1825 return $result;
1829 * Describes the get_quiz_feedback_for_grade return value.
1831 * @return external_single_structure
1832 * @since Moodle 3.1
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
1849 * @since Moodle 3.1
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
1864 * @since Moodle 3.1
1866 public static function get_quiz_access_information($quizid) {
1867 global $DB, $USER;
1869 $warnings = [];
1871 $params = [
1872 'quizid' => $quizid
1874 $params = self::validate_parameters(self::get_quiz_access_information_parameters(), $params);
1876 list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
1878 $result = [];
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);
1889 $timenow = time();
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;
1897 return $result;
1901 * Describes the get_quiz_access_information return value.
1903 * @return external_single_structure
1904 * @since Moodle 3.1
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
1913 or not.'),
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
1930 * @since Moodle 3.1
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
1947 * @since Moodle 3.1
1949 public static function get_attempt_access_information($quizid, $attemptid = 0) {
1950 global $DB, $USER;
1952 $warnings = [];
1954 $params = [
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);
1974 $timenow = time();
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;
1995 $result = [];
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;
2007 return $result;
2011 * Describes the get_attempt_access_information return value.
2013 * @return external_single_structure
2014 * @since Moodle 3.1
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).',
2020 VALUE_OPTIONAL),
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'),
2026 'list of reasons'),
2027 'warnings' => new external_warnings(),
2033 * Describes the parameters for get_quiz_required_qtypes.
2035 * @return external_function_parameters
2036 * @since Moodle 3.1
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
2052 * @since Moodle 3.1
2054 public static function get_quiz_required_qtypes($quizid) {
2055 global $DB, $USER;
2057 $warnings = [];
2059 $params = [
2060 'quizid' => $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.
2070 $result = [];
2071 $result['questiontypes'] = $quizobj->get_all_question_types_used(true);
2072 $result['warnings'] = $warnings;
2073 return $result;
2077 * Describes the get_quiz_required_qtypes return value.
2079 * @return external_single_structure
2080 * @since Moodle 3.1
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(),