Merge branch 'MDL-53034-master' of git://github.com/jleyva/moodle
[moodle.git] / mod / quiz / classes / external.php
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.
13 //
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
19  *
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
25  */
27 defined('MOODLE_INTERNAL') || die;
29 require_once($CFG->libdir . '/externallib.php');
30 require_once($CFG->dirroot . '/mod/quiz/locallib.php');
32 /**
33  * Quiz external functions
34  *
35  * @package    mod_quiz
36  * @category   external
37  * @copyright  2016 Juan Leyva <juan@moodle.com>
38  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
39  * @since      Moodle 3.1
40  */
41 class mod_quiz_external extends external_api {
43     /**
44      * Describes the parameters for get_quizzes_by_courses.
45      *
46      * @return external_external_function_parameters
47      * @since Moodle 3.1
48      */
49     public static function get_quizzes_by_courses_parameters() {
50         return new external_function_parameters (
51             array(
52                 'courseids' => new external_multiple_structure(
53                     new external_value(PARAM_INT, 'course id'), 'Array of course ids', VALUE_DEFAULT, array()
54                 ),
55             )
56         );
57     }
59     /**
60      * Returns a list of quizzes in a provided list of courses,
61      * if no list is provided all quizzes that the user can view will be returned.
62      *
63      * @param array $courseids Array of course ids
64      * @return array of quizzes details
65      * @since Moodle 3.1
66      */
67     public static function get_quizzes_by_courses($courseids = array()) {
68         global $USER;
70         $warnings = array();
71         $returnedquizzes = array();
73         $params = array(
74             'courseids' => $courseids,
75         );
76         $params = self::validate_parameters(self::get_quizzes_by_courses_parameters(), $params);
78         $mycourses = array();
79         if (empty($params['courseids'])) {
80             $mycourses = enrol_get_my_courses();
81             $params['courseids'] = array_keys($mycourses);
82         }
84         // Ensure there are courseids to loop through.
85         if (!empty($params['courseids'])) {
87             list($courses, $warnings) = external_util::validate_courses($params['courseids'], $mycourses);
89             // Get the quizzes in this course, this function checks users visibility permissions.
90             // We can avoid then additional validate_context calls.
91             $quizzes = get_all_instances_in_courses("quiz", $courses);
92             foreach ($quizzes as $quiz) {
93                 $context = context_module::instance($quiz->coursemodule);
95                 // Update quiz with override information.
96                 $quiz = quiz_update_effective_access($quiz, $USER->id);
98                 // Entry to return.
99                 $quizdetails = array();
100                 // First, we return information that any user can see in the web interface.
101                 $quizdetails['id'] = $quiz->id;
102                 $quizdetails['coursemodule']      = $quiz->coursemodule;
103                 $quizdetails['course']            = $quiz->course;
104                 $quizdetails['name']              = external_format_string($quiz->name, $context->id);
106                 if (has_capability('mod/quiz:view', $context)) {
107                     // Format intro.
108                     list($quizdetails['intro'], $quizdetails['introformat']) = external_format_text($quiz->intro,
109                                                                     $quiz->introformat, $context->id, 'mod_quiz', 'intro', null);
111                     $viewablefields = array('timeopen', 'timeclose', 'grademethod', 'section', 'visible', 'groupmode',
112                                             'groupingid');
114                     $timenow = time();
115                     $quizobj = quiz::create($quiz->id, $USER->id);
116                     $accessmanager = new quiz_access_manager($quizobj, $timenow, has_capability('mod/quiz:ignoretimelimits',
117                                                                 $context, null, false));
119                     // Fields the user could see if have access to the quiz.
120                     if (!$accessmanager->prevent_access()) {
121                         // Some times this function returns just empty.
122                         $hasfeedback = quiz_has_feedback($quiz);
123                         $quizdetails['hasfeedback'] = (!empty($hasfeedback)) ? 1 : 0;
124                         $quizdetails['hasquestions'] = (int) $quizobj->has_questions();
125                         $quizdetails['autosaveperiod'] = get_config('quiz', 'autosaveperiod');
127                         $additionalfields = array('timelimit', 'attempts', 'attemptonlast', 'grademethod', 'decimalpoints',
128                                                     'questiondecimalpoints', 'reviewattempt', 'reviewcorrectness', 'reviewmarks',
129                                                     'reviewspecificfeedback', 'reviewgeneralfeedback', 'reviewrightanswer',
130                                                     'reviewoverallfeedback', 'questionsperpage', 'navmethod', 'sumgrades', 'grade',
131                                                     'browsersecurity', 'delay1', 'delay2', 'showuserpicture', 'showblocks',
132                                                     'completionattemptsexhausted', 'completionpass', 'overduehandling',
133                                                     'graceperiod', 'preferredbehaviour', 'canredoquestions');
134                         $viewablefields = array_merge($viewablefields, $additionalfields);
135                     }
137                     // Fields only for managers.
138                     if (has_capability('moodle/course:manageactivities', $context)) {
139                         $additionalfields = array('shuffleanswers', 'timecreated', 'timemodified', 'password', 'subnet');
140                         $viewablefields = array_merge($viewablefields, $additionalfields);
141                     }
143                     foreach ($viewablefields as $field) {
144                         $quizdetails[$field] = $quiz->{$field};
145                     }
146                 }
147                 $returnedquizzes[] = $quizdetails;
148             }
149         }
150         $result = array();
151         $result['quizzes'] = $returnedquizzes;
152         $result['warnings'] = $warnings;
153         return $result;
154     }
156     /**
157      * Describes the get_quizzes_by_courses return value.
158      *
159      * @return external_single_structure
160      * @since Moodle 3.1
161      */
162     public static function get_quizzes_by_courses_returns() {
163         return new external_single_structure(
164             array(
165                 'quizzes' => new external_multiple_structure(
166                     new external_single_structure(
167                         array(
168                             'id' => new external_value(PARAM_INT, 'Standard Moodle primary key.'),
169                             'course' => new external_value(PARAM_INT, 'Foreign key reference to the course this quiz is part of.'),
170                             'coursemodule' => new external_value(PARAM_INT, 'Course module id.'),
171                             'name' => new external_value(PARAM_RAW, 'Quiz name.'),
172                             'intro' => new external_value(PARAM_RAW, 'Quiz introduction text.', VALUE_OPTIONAL),
173                             'introformat' => new external_format_value('intro', VALUE_OPTIONAL),
174                             'timeopen' => new external_value(PARAM_INT, 'The time when this quiz opens. (0 = no restriction.)',
175                                                                 VALUE_OPTIONAL),
176                             'timeclose' => new external_value(PARAM_INT, 'The time when this quiz closes. (0 = no restriction.)',
177                                                                 VALUE_OPTIONAL),
178                             'timelimit' => new external_value(PARAM_INT, 'The time limit for quiz attempts, in seconds.',
179                                                                 VALUE_OPTIONAL),
180                             'overduehandling' => new external_value(PARAM_ALPHA, 'The method used to handle overdue attempts.
181                                                                     \'autosubmit\', \'graceperiod\' or \'autoabandon\'.',
182                                                                     VALUE_OPTIONAL),
183                             'graceperiod' => new external_value(PARAM_INT, 'The amount of time (in seconds) after the time limit
184                                                                 runs out during which attempts can still be submitted,
185                                                                 if overduehandling is set to allow it.', VALUE_OPTIONAL),
186                             'preferredbehaviour' => new external_value(PARAM_ALPHANUMEXT, 'The behaviour to ask questions to use.',
187                                                                         VALUE_OPTIONAL),
188                             'canredoquestions' => new external_value(PARAM_INT, 'Allows students to redo any completed question
189                                                                         within a quiz attempt.', VALUE_OPTIONAL),
190                             'attempts' => new external_value(PARAM_INT, 'The maximum number of attempts a student is allowed.',
191                                                                 VALUE_OPTIONAL),
192                             'attemptonlast' => new external_value(PARAM_INT, 'Whether subsequent attempts start from the answer
193                                                                     to the previous attempt (1) or start blank (0).',
194                                                                     VALUE_OPTIONAL),
195                             'grademethod' => new external_value(PARAM_INT, 'One of the values QUIZ_GRADEHIGHEST, QUIZ_GRADEAVERAGE,
196                                                                     QUIZ_ATTEMPTFIRST or QUIZ_ATTEMPTLAST.', VALUE_OPTIONAL),
197                             'decimalpoints' => new external_value(PARAM_INT, 'Number of decimal points to use when displaying
198                                                                     grades.', VALUE_OPTIONAL),
199                             'questiondecimalpoints' => new external_value(PARAM_INT, 'Number of decimal points to use when
200                                                                             displaying question grades.
201                                                                             (-1 means use decimalpoints.)', VALUE_OPTIONAL),
202                             'reviewattempt' => new external_value(PARAM_INT, 'Whether users are allowed to review their quiz
203                                                                     attempts at various times. This is a bit field, decoded by the
204                                                                     mod_quiz_display_options class. It is formed by ORing together
205                                                                     the constants defined there.', VALUE_OPTIONAL),
206                             'reviewcorrectness' => new external_value(PARAM_INT, 'Whether users are allowed to review their quiz
207                                                                         attempts at various times.
208                                                                         A bit field, like reviewattempt.', VALUE_OPTIONAL),
209                             'reviewmarks' => new external_value(PARAM_INT, 'Whether users are allowed to review their quiz attempts
210                                                                 at various times. A bit field, like reviewattempt.',
211                                                                 VALUE_OPTIONAL),
212                             'reviewspecificfeedback' => new external_value(PARAM_INT, 'Whether users are allowed to review their
213                                                                             quiz attempts at various times. A bit field, like
214                                                                             reviewattempt.', VALUE_OPTIONAL),
215                             'reviewgeneralfeedback' => new external_value(PARAM_INT, 'Whether users are allowed to review their
216                                                                             quiz attempts at various times. A bit field, like
217                                                                             reviewattempt.', VALUE_OPTIONAL),
218                             'reviewrightanswer' => new external_value(PARAM_INT, 'Whether users are allowed to review their quiz
219                                                                         attempts at various times. A bit field, like
220                                                                         reviewattempt.', VALUE_OPTIONAL),
221                             'reviewoverallfeedback' => new external_value(PARAM_INT, 'Whether users are allowed to review their quiz
222                                                                             attempts at various times. A bit field, like
223                                                                             reviewattempt.', VALUE_OPTIONAL),
224                             'questionsperpage' => new external_value(PARAM_INT, 'How often to insert a page break when editing
225                                                                         the quiz, or when shuffling the question order.',
226                                                                         VALUE_OPTIONAL),
227                             'navmethod' => new external_value(PARAM_ALPHA, 'Any constraints on how the user is allowed to navigate
228                                                                 around the quiz. Currently recognised values are
229                                                                 \'free\' and \'seq\'.', VALUE_OPTIONAL),
230                             'shuffleanswers' => new external_value(PARAM_INT, 'Whether the parts of the question should be shuffled,
231                                                                     in those question types that support it.', VALUE_OPTIONAL),
232                             'sumgrades' => new external_value(PARAM_FLOAT, 'The total of all the question instance maxmarks.',
233                                                                 VALUE_OPTIONAL),
234                             'grade' => new external_value(PARAM_FLOAT, 'The total that the quiz overall grade is scaled to be
235                                                             out of.', VALUE_OPTIONAL),
236                             'timecreated' => new external_value(PARAM_INT, 'The time when the quiz was added to the course.',
237                                                                 VALUE_OPTIONAL),
238                             'timemodified' => new external_value(PARAM_INT, 'Last modified time.',
239                                                                     VALUE_OPTIONAL),
240                             'password' => new external_value(PARAM_RAW, 'A password that the student must enter before starting or
241                                                                 continuing a quiz attempt.', VALUE_OPTIONAL),
242                             'subnet' => new external_value(PARAM_RAW, 'Used to restrict the IP addresses from which this quiz can
243                                                             be attempted. The format is as requried by the address_in_subnet
244                                                             function.', VALUE_OPTIONAL),
245                             'browsersecurity' => new external_value(PARAM_ALPHANUMEXT, 'Restriciton on the browser the student must
246                                                                     use. E.g. \'securewindow\'.', VALUE_OPTIONAL),
247                             'delay1' => new external_value(PARAM_INT, 'Delay that must be left between the first and second attempt,
248                                                             in seconds.', VALUE_OPTIONAL),
249                             'delay2' => new external_value(PARAM_INT, 'Delay that must be left between the second and subsequent
250                                                             attempt, in seconds.', VALUE_OPTIONAL),
251                             'showuserpicture' => new external_value(PARAM_INT, 'Option to show the user\'s picture during the
252                                                                     attempt and on the review page.', VALUE_OPTIONAL),
253                             'showblocks' => new external_value(PARAM_INT, 'Whether blocks should be shown on the attempt.php and
254                                                                 review.php pages.', VALUE_OPTIONAL),
255                             'completionattemptsexhausted' => new external_value(PARAM_INT, 'Mark quiz complete when the student has
256                                                                                 exhausted the maximum number of attempts',
257                                                                                 VALUE_OPTIONAL),
258                             'completionpass' => new external_value(PARAM_INT, 'Whether to require passing grade', VALUE_OPTIONAL),
259                             'autosaveperiod' => new external_value(PARAM_INT, 'Auto-save delay', VALUE_OPTIONAL),
260                             'hasfeedback' => new external_value(PARAM_INT, 'Whether the quiz has any non-blank feedback text',
261                                                                 VALUE_OPTIONAL),
262                             'hasquestions' => new external_value(PARAM_INT, 'Whether the quiz has questions', VALUE_OPTIONAL),
263                             'section' => new external_value(PARAM_INT, 'Course section id', VALUE_OPTIONAL),
264                             'visible' => new external_value(PARAM_INT, 'Module visibility', VALUE_OPTIONAL),
265                             'groupmode' => new external_value(PARAM_INT, 'Group mode', VALUE_OPTIONAL),
266                             'groupingid' => new external_value(PARAM_INT, 'Grouping id', VALUE_OPTIONAL),
267                         )
268                     )
269                 ),
270                 'warnings' => new external_warnings(),
271             )
272         );
273     }
275     /**
276      * Describes the parameters for view_quiz.
277      *
278      * @return external_external_function_parameters
279      * @since Moodle 3.1
280      */
281     public static function view_quiz_parameters() {
282         return new external_function_parameters (
283             array(
284                 'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
285             )
286         );
287     }
289     /**
290      * Trigger the course module viewed event and update the module completion status.
291      *
292      * @param int $quizid quiz instance id
293      * @return array of warnings and status result
294      * @since Moodle 3.1
295      * @throws moodle_exception
296      */
297     public static function view_quiz($quizid) {
298         global $DB;
300         $params = self::validate_parameters(self::view_quiz_parameters(), array('quizid' => $quizid));
301         $warnings = array();
303         // Request and permission validation.
304         $quiz = $DB->get_record('quiz', array('id' => $params['quizid']), '*', MUST_EXIST);
305         list($course, $cm) = get_course_and_cm_from_instance($quiz, 'quiz');
307         $context = context_module::instance($cm->id);
308         self::validate_context($context);
310         // Trigger course_module_viewed event and completion.
311         quiz_view($quiz, $course, $cm, $context);
313         $result = array();
314         $result['status'] = true;
315         $result['warnings'] = $warnings;
316         return $result;
317     }
319     /**
320      * Describes the view_quiz return value.
321      *
322      * @return external_single_structure
323      * @since Moodle 3.1
324      */
325     public static function view_quiz_returns() {
326         return new external_single_structure(
327             array(
328                 'status' => new external_value(PARAM_BOOL, 'status: true if success'),
329                 'warnings' => new external_warnings(),
330             )
331         );
332     }
334     /**
335      * Describes the parameters for get_user_attempts.
336      *
337      * @return external_external_function_parameters
338      * @since Moodle 3.1
339      */
340     public static function get_user_attempts_parameters() {
341         return new external_function_parameters (
342             array(
343                 'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
344                 'userid' => new external_value(PARAM_INT, 'user id, empty for current user', VALUE_DEFAULT, 0),
345                 'status' => new external_value(PARAM_ALPHA, 'quiz status: all, finished or unfinished', VALUE_DEFAULT, 'finished'),
346                 'includepreviews' => new external_value(PARAM_BOOL, 'whether to include previews or not', VALUE_DEFAULT, false),
348             )
349         );
350     }
352     /**
353      * Return a list of attempts for the given quiz and user.
354      *
355      * @param int $quizid quiz instance id
356      * @param int $userid user id
357      * @param string $status quiz status: all, finished or unfinished
358      * @param bool $includepreviews whether to include previews or not
359      * @return array of warnings and the list of attempts
360      * @since Moodle 3.1
361      * @throws invalid_parameter_exception
362      */
363     public static function get_user_attempts($quizid, $userid = 0, $status = 'finished', $includepreviews = false) {
364         global $DB, $USER;
366         $warnings = array();
368         $params = array(
369             'quizid' => $quizid,
370             'userid' => $userid,
371             'status' => $status,
372             'includepreviews' => $includepreviews,
373         );
374         $params = self::validate_parameters(self::get_user_attempts_parameters(), $params);
376         // Request and permission validation.
377         $quiz = $DB->get_record('quiz', array('id' => $params['quizid']), '*', MUST_EXIST);
378         list($course, $cm) = get_course_and_cm_from_instance($quiz, 'quiz');
380         $context = context_module::instance($cm->id);
381         self::validate_context($context);
383         if (!in_array($params['status'], array('all', 'finished', 'unfinished'))) {
384             throw new invalid_parameter_exception('Invalid status value');
385         }
387         // Default value for userid.
388         if (empty($params['userid'])) {
389             $params['userid'] = $USER->id;
390         }
392         $user = core_user::get_user($params['userid'], '*', MUST_EXIST);
393         core_user::require_active_user($user);
395         // Extra checks so only users with permissions can view other users attempts.
396         if ($USER->id != $user->id) {
397             require_capability('mod/quiz:viewreports', $context);
398         }
400         $attempts = quiz_get_user_attempts($quiz->id, $user->id, $params['status'], $params['includepreviews']);
402         $result = array();
403         $result['attempts'] = $attempts;
404         $result['warnings'] = $warnings;
405         return $result;
406     }
408     /**
409      * Describes a single attempt structure.
410      *
411      * @return external_single_structure the attempt structure
412      */
413     private static function attempt_structure() {
414         return new external_single_structure(
415             array(
416                 'id' => new external_value(PARAM_INT, 'Attempt id.', VALUE_OPTIONAL),
417                 'quiz' => new external_value(PARAM_INT, 'Foreign key reference to the quiz that was attempted.',
418                                                 VALUE_OPTIONAL),
419                 'userid' => new external_value(PARAM_INT, 'Foreign key reference to the user whose attempt this is.',
420                                                 VALUE_OPTIONAL),
421                 'attempt' => new external_value(PARAM_INT, 'Sequentially numbers this students attempts at this quiz.',
422                                                 VALUE_OPTIONAL),
423                 'uniqueid' => new external_value(PARAM_INT, 'Foreign key reference to the question_usage that holds the
424                                                     details of the the question_attempts that make up this quiz
425                                                     attempt.', VALUE_OPTIONAL),
426                 'layout' => new external_value(PARAM_RAW, 'Attempt layout.', VALUE_OPTIONAL),
427                 'currentpage' => new external_value(PARAM_INT, 'Attempt current page.', VALUE_OPTIONAL),
428                 'preview' => new external_value(PARAM_INT, 'Whether is a preview attempt or not.', VALUE_OPTIONAL),
429                 'state' => new external_value(PARAM_ALPHA, 'The current state of the attempts. \'inprogress\',
430                                                 \'overdue\', \'finished\' or \'abandoned\'.', VALUE_OPTIONAL),
431                 'timestart' => new external_value(PARAM_INT, 'Time when the attempt was started.', VALUE_OPTIONAL),
432                 'timefinish' => new external_value(PARAM_INT, 'Time when the attempt was submitted.
433                                                     0 if the attempt has not been submitted yet.', VALUE_OPTIONAL),
434                 'timemodified' => new external_value(PARAM_INT, 'Last modified time.', VALUE_OPTIONAL),
435                 'timecheckstate' => new external_value(PARAM_INT, 'Next time quiz cron should check attempt for
436                                                         state changes.  NULL means never check.', VALUE_OPTIONAL),
437                 'sumgrades' => new external_value(PARAM_FLOAT, 'Total marks for this attempt.', VALUE_OPTIONAL),
438             )
439         );
440     }
442     /**
443      * Describes the get_user_attempts return value.
444      *
445      * @return external_single_structure
446      * @since Moodle 3.1
447      */
448     public static function get_user_attempts_returns() {
449         return new external_single_structure(
450             array(
451                 'attempts' => new external_multiple_structure(self::attempt_structure()),
452                 'warnings' => new external_warnings(),
453             )
454         );
455     }
457     /**
458      * Describes the parameters for get_user_best_grade.
459      *
460      * @return external_external_function_parameters
461      * @since Moodle 3.1
462      */
463     public static function get_user_best_grade_parameters() {
464         return new external_function_parameters (
465             array(
466                 'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
467                 'userid' => new external_value(PARAM_INT, 'user id', VALUE_DEFAULT, 0),
468             )
469         );
470     }
472     /**
473      * Get the best current grade for the given user on a quiz.
474      *
475      * @param int $quizid quiz instance id
476      * @param int $userid user id
477      * @return array of warnings and the grade information
478      * @since Moodle 3.1
479      */
480     public static function get_user_best_grade($quizid, $userid = 0) {
481         global $DB, $USER;
483         $warnings = array();
485         $params = array(
486             'quizid' => $quizid,
487             'userid' => $userid,
488         );
489         $params = self::validate_parameters(self::get_user_best_grade_parameters(), $params);
491         // Request and permission validation.
492         $quiz = $DB->get_record('quiz', array('id' => $params['quizid']), '*', MUST_EXIST);
493         list($course, $cm) = get_course_and_cm_from_instance($quiz, 'quiz');
495         $context = context_module::instance($cm->id);
496         self::validate_context($context);
498         // Default value for userid.
499         if (empty($params['userid'])) {
500             $params['userid'] = $USER->id;
501         }
503         $user = core_user::get_user($params['userid'], '*', MUST_EXIST);
504         core_user::require_active_user($user);
506         // Extra checks so only users with permissions can view other users attempts.
507         if ($USER->id != $user->id) {
508             require_capability('mod/quiz:viewreports', $context);
509         }
511         $result = array();
512         $grade = quiz_get_best_grade($quiz, $user->id);
514         if ($grade === null) {
515             $result['hasgrade'] = false;
516         } else {
517             $result['hasgrade'] = true;
518             $result['grade'] = $grade;
519         }
520         $result['warnings'] = $warnings;
521         return $result;
522     }
524     /**
525      * Describes the get_user_best_grade return value.
526      *
527      * @return external_single_structure
528      * @since Moodle 3.1
529      */
530     public static function get_user_best_grade_returns() {
531         return new external_single_structure(
532             array(
533                 'hasgrade' => new external_value(PARAM_BOOL, 'Whether the user has a grade on the given quiz.'),
534                 'grade' => new external_value(PARAM_FLOAT, 'The grade (only if the user has a grade).', VALUE_OPTIONAL),
535                 'warnings' => new external_warnings(),
536             )
537         );
538     }
540     /**
541      * Describes the parameters for get_combined_review_options.
542      *
543      * @return external_external_function_parameters
544      * @since Moodle 3.1
545      */
546     public static function get_combined_review_options_parameters() {
547         return new external_function_parameters (
548             array(
549                 'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
550                 'userid' => new external_value(PARAM_INT, 'user id (empty for current user)', VALUE_DEFAULT, 0),
552             )
553         );
554     }
556     /**
557      * Combines the review options from a number of different quiz attempts.
558      *
559      * @param int $quizid quiz instance id
560      * @param int $userid user id (empty for current user)
561      * @return array of warnings and the review options
562      * @since Moodle 3.1
563      */
564     public static function get_combined_review_options($quizid, $userid = 0) {
565         global $DB, $USER;
567         $warnings = array();
569         $params = array(
570             'quizid' => $quizid,
571             'userid' => $userid,
572         );
573         $params = self::validate_parameters(self::get_combined_review_options_parameters(), $params);
575         // Request and permission validation.
576         $quiz = $DB->get_record('quiz', array('id' => $params['quizid']), '*', MUST_EXIST);
577         list($course, $cm) = get_course_and_cm_from_instance($quiz, 'quiz');
579         $context = context_module::instance($cm->id);
580         self::validate_context($context);
582         // Default value for userid.
583         if (empty($params['userid'])) {
584             $params['userid'] = $USER->id;
585         }
587         $user = core_user::get_user($params['userid'], '*', MUST_EXIST);
588         core_user::require_active_user($user);
590         // Extra checks so only users with permissions can view other users attempts.
591         if ($USER->id != $user->id) {
592             require_capability('mod/quiz:viewreports', $context);
593         }
595         $attempts = quiz_get_user_attempts($quiz->id, $user->id, 'all', true);
597         $result = array();
598         $result['someoptions'] = [];
599         $result['alloptions'] = [];
601         list($someoptions, $alloptions) = quiz_get_combined_reviewoptions($quiz, $attempts);
603         foreach (array('someoptions', 'alloptions') as $typeofoption) {
604             foreach ($$typeofoption as $key => $value) {
605                 $result[$typeofoption][] = array(
606                     "name" => $key,
607                     "value" => (!empty($value)) ? $value : 0
608                 );
609             }
610         }
612         $result['warnings'] = $warnings;
613         return $result;
614     }
616     /**
617      * Describes the get_combined_review_options return value.
618      *
619      * @return external_single_structure
620      * @since Moodle 3.1
621      */
622     public static function get_combined_review_options_returns() {
623         return new external_single_structure(
624             array(
625                 'someoptions' => new external_multiple_structure(
626                     new external_single_structure(
627                         array(
628                             'name' => new external_value(PARAM_ALPHANUMEXT, 'option name'),
629                             'value' => new external_value(PARAM_INT, 'option value'),
630                         )
631                     )
632                 ),
633                 'alloptions' => new external_multiple_structure(
634                     new external_single_structure(
635                         array(
636                             'name' => new external_value(PARAM_ALPHANUMEXT, 'option name'),
637                             'value' => new external_value(PARAM_INT, 'option value'),
638                         )
639                     )
640                 ),
641                 'warnings' => new external_warnings(),
642             )
643         );
644     }
646     /**
647      * Describes the parameters for start_attempt.
648      *
649      * @return external_external_function_parameters
650      * @since Moodle 3.1
651      */
652     public static function start_attempt_parameters() {
653         return new external_function_parameters (
654             array(
655                 'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
656                 'preflightdata' => new external_multiple_structure(
657                     new external_single_structure(
658                         array(
659                             'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
660                             'value' => new external_value(PARAM_RAW, 'data value'),
661                         )
662                     ), 'Preflight required data (like passwords)', VALUE_DEFAULT, array()
663                 ),
664                 'forcenew' => new external_value(PARAM_BOOL, 'Whether to force a new attempt or not.', VALUE_DEFAULT, false),
666             )
667         );
668     }
670     /**
671      * Starts a new attempt at a quiz.
672      *
673      * @param int $quizid quiz instance id
674      * @param array $preflightdata preflight required data (like passwords)
675      * @param bool $forcenew Whether to force a new attempt or not.
676      * @return array of warnings and the attempt basic data
677      * @since Moodle 3.1
678      * @throws moodle_quiz_exception
679      */
680     public static function start_attempt($quizid, $preflightdata = array(), $forcenew = false) {
681         global $DB, $USER;
683         $warnings = array();
684         $attempt = array();
686         $params = array(
687             'quizid' => $quizid,
688             'preflightdata' => $preflightdata,
689             'forcenew' => $forcenew,
690         );
691         $params = self::validate_parameters(self::start_attempt_parameters(), $params);
692         $forcenew = $params['forcenew'];
694         // Request and permission validation.
695         $quiz = $DB->get_record('quiz', array('id' => $params['quizid']), '*', MUST_EXIST);
696         list($course, $cm) = get_course_and_cm_from_instance($quiz, 'quiz');
698         $context = context_module::instance($cm->id);
699         self::validate_context($context);
701         $quizobj = quiz::create($cm->instance, $USER->id);
703         // Check questions.
704         if (!$quizobj->has_questions()) {
705             throw new moodle_quiz_exception($quizobj, 'noquestionsfound');
706         }
708         // Create an object to manage all the other (non-roles) access rules.
709         $timenow = time();
710         $accessmanager = $quizobj->get_access_manager($timenow);
712         // Validate permissions for creating a new attempt and start a new preview attempt if required.
713         list($currentattemptid, $attemptnumber, $lastattempt, $messages, $page) =
714             quiz_validate_new_attempt($quizobj, $accessmanager, $forcenew, -1, false);
716         // Check access.
717         if (!$quizobj->is_preview_user() && $messages) {
718             // Create warnings with the exact messages.
719             foreach ($messages as $message) {
720                 $warnings[] = array(
721                     'item' => 'quiz',
722                     'itemid' => $quiz->id,
723                     'warningcode' => '1',
724                     'message' => clean_text($message, PARAM_TEXT)
725                 );
726             }
727         } else {
728             if ($accessmanager->is_preflight_check_required($currentattemptid)) {
729                 // Need to do some checks before allowing the user to continue.
731                 $provideddata = array();
732                 foreach ($params['preflightdata'] as $data) {
733                     $provideddata[$data['name']] = $data['value'];
734                 }
736                 $errors = $accessmanager->validate_preflight_check($provideddata, [], $currentattemptid);
738                 if (!empty($errors)) {
739                     throw new moodle_quiz_exception($quizobj, array_shift($errors));
740                 }
742                 // Pre-flight check passed.
743                 $accessmanager->notify_preflight_check_passed($currentattemptid);
744             }
746             if ($currentattemptid) {
747                 if ($lastattempt->state == quiz_attempt::OVERDUE) {
748                     throw new moodle_quiz_exception($quizobj, 'stateoverdue');
749                 } else {
750                     throw new moodle_quiz_exception($quizobj, 'attemptstillinprogress');
751                 }
752             }
753             $attempt = quiz_prepare_and_start_new_attempt($quizobj, $attemptnumber, $lastattempt);
754         }
756         $result = array();
757         $result['attempt'] = $attempt;
758         $result['warnings'] = $warnings;
759         return $result;
760     }
762     /**
763      * Describes the start_attempt return value.
764      *
765      * @return external_single_structure
766      * @since Moodle 3.1
767      */
768     public static function start_attempt_returns() {
769         return new external_single_structure(
770             array(
771                 'attempt' => self::attempt_structure(),
772                 'warnings' => new external_warnings(),
773             )
774         );
775     }
777     /**
778      * Utility function for validating a given attempt
779      *
780      * @param  array $params array of parameters including the attemptid and preflight data
781      * @param  bool $checkaccessrules whether to check the quiz access rules or not
782      * @param  bool $failifoverdue whether to return error if the attempt is overdue
783      * @return  array containing the attempt object and access messages
784      * @throws moodle_quiz_exception
785      * @since  Moodle 3.1
786      */
787     protected static function validate_attempt($params, $checkaccessrules = true, $failifoverdue = true) {
788         global $USER;
790         $attemptobj = quiz_attempt::create($params['attemptid']);
792         $context = context_module::instance($attemptobj->get_cm()->id);
793         self::validate_context($context);
795         // Check that this attempt belongs to this user.
796         if ($attemptobj->get_userid() != $USER->id) {
797             throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'notyourattempt');
798         }
800         // General capabilities check.
801         $ispreviewuser = $attemptobj->is_preview_user();
802         if (!$ispreviewuser) {
803             $attemptobj->require_capability('mod/quiz:attempt');
804         }
806         // Check the access rules.
807         $accessmanager = $attemptobj->get_access_manager(time());
808         $messages = array();
809         if ($checkaccessrules) {
810             // If the attempt is now overdue, or abandoned, deal with that.
811             $attemptobj->handle_if_time_expired(time(), true);
813             $messages = $accessmanager->prevent_access();
814             if (!$ispreviewuser && $messages) {
815                 throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'attempterror');
816             }
817         }
819         // Attempt closed?.
820         if ($attemptobj->is_finished()) {
821             throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'attemptalreadyclosed');
822         } else if ($failifoverdue && $attemptobj->get_state() == quiz_attempt::OVERDUE) {
823             throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'stateoverdue');
824         }
826         // User submitted data (like the quiz password).
827         if ($accessmanager->is_preflight_check_required($attemptobj->get_attemptid())) {
828             $provideddata = array();
829             foreach ($params['preflightdata'] as $data) {
830                 $provideddata[$data['name']] = $data['value'];
831             }
833             $errors = $accessmanager->validate_preflight_check($provideddata, [], $params['attemptid']);
834             if (!empty($errors)) {
835                 throw new moodle_quiz_exception($attemptobj->get_quizobj(), array_shift($errors));
836             }
837             // Pre-flight check passed.
838             $accessmanager->notify_preflight_check_passed($params['attemptid']);
839         }
841         if (isset($params['page'])) {
842             // Check if the page is out of range.
843             if ($params['page'] != $attemptobj->force_page_number_into_range($params['page'])) {
844                 throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'Invalid page number');
845             }
847             // Prevent out of sequence access.
848             if (!$attemptobj->check_page_access($params['page'])) {
849                 throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'Out of sequence access');
850             }
852             // Check slots.
853             $slots = $attemptobj->get_slots($params['page']);
855             if (empty($slots)) {
856                 throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'noquestionsfound');
857             }
858         }
860         return array($attemptobj, $messages);
861     }
863     /**
864      * Describes a single question structure.
865      *
866      * @return external_single_structure the question structure
867      * @since  Moodle 3.1
868      */
869     private static function question_structure() {
870         return new external_single_structure(
871             array(
872                 'slot' => new external_value(PARAM_INT, 'slot number'),
873                 'type' => new external_value(PARAM_ALPHANUMEXT, 'question type, i.e: multichoice'),
874                 'page' => new external_value(PARAM_INT, 'page of the quiz this question appears on'),
875                 'html' => new external_value(PARAM_RAW, 'the question rendered'),
876                 'flagged' => new external_value(PARAM_BOOL, 'whether the question is flagged or not'),
877                 'number' => new external_value(PARAM_INT, 'question ordering number in the quiz', VALUE_OPTIONAL),
878                 'state' => new external_value(PARAM_ALPHA, 'the state where the question is in', VALUE_OPTIONAL),
879                 'status' => new external_value(PARAM_RAW, 'current formatted state of the question', VALUE_OPTIONAL),
880                 'mark' => new external_value(PARAM_RAW, 'the mark awarded', VALUE_OPTIONAL),
881                 'maxmark' => new external_value(PARAM_FLOAT, 'the maximum mark possible for this question attempt', VALUE_OPTIONAL),
882             )
883         );
884     }
886     /**
887      * Return questions information for a given attempt.
888      *
889      * @param  quiz_attempt  $attemptobj  the quiz attempt object
890      * @param  bool  $review  whether if we are in review mode or not
891      * @param  mixed  $page  string 'all' or integer page number
892      * @return array array of questions including data
893      */
894     private static function get_attempt_questions_data(quiz_attempt $attemptobj, $review, $page = 'all') {
895         global $PAGE;
897         $questions = array();
898         $contextid = $attemptobj->get_quizobj()->get_context()->id;
899         $displayoptions = $attemptobj->get_display_options($review);
900         $renderer = $PAGE->get_renderer('mod_quiz');
902         foreach ($attemptobj->get_slots($page) as $slot) {
904             $question = array(
905                 'slot' => $slot,
906                 'type' => $attemptobj->get_question_type_name($slot),
907                 'page' => $attemptobj->get_question_page($slot),
908                 'flagged' => $attemptobj->is_question_flagged($slot),
909                 'html' => $attemptobj->render_question($slot, $review, $renderer) . $PAGE->requires->get_end_code()
910             );
912             if ($attemptobj->is_real_question($slot)) {
913                 $question['number'] = $attemptobj->get_question_number($slot);
914                 $question['state'] = (string) $attemptobj->get_question_state($slot);
915                 $question['status'] = $attemptobj->get_question_status($slot, $displayoptions->correctness);
916             }
917             if ($displayoptions->marks >= question_display_options::MAX_ONLY) {
918                 $question['maxmark'] = $attemptobj->get_question_attempt($slot)->get_max_mark();
919             }
920             if ($displayoptions->marks >= question_display_options::MARK_AND_MAX) {
921                 $question['mark'] = $attemptobj->get_question_mark($slot);
922             }
924             $questions[] = $question;
925         }
926         return $questions;
927     }
929     /**
930      * Describes the parameters for get_attempt_data.
931      *
932      * @return external_external_function_parameters
933      * @since Moodle 3.1
934      */
935     public static function get_attempt_data_parameters() {
936         return new external_function_parameters (
937             array(
938                 'attemptid' => new external_value(PARAM_INT, 'attempt id'),
939                 'page' => new external_value(PARAM_INT, 'page number'),
940                 'preflightdata' => new external_multiple_structure(
941                     new external_single_structure(
942                         array(
943                             'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
944                             'value' => new external_value(PARAM_RAW, 'data value'),
945                         )
946                     ), 'Preflight required data (like passwords)', VALUE_DEFAULT, array()
947                 )
948             )
949         );
950     }
952     /**
953      * Returns information for the given attempt page for a quiz attempt in progress.
954      *
955      * @param int $attemptid attempt id
956      * @param int $page page number
957      * @param array $preflightdata preflight required data (like passwords)
958      * @return array of warnings and the attempt data, next page, message and questions
959      * @since Moodle 3.1
960      * @throws moodle_quiz_exceptions
961      */
962     public static function get_attempt_data($attemptid, $page, $preflightdata = array()) {
964         $warnings = array();
966         $params = array(
967             'attemptid' => $attemptid,
968             'page' => $page,
969             'preflightdata' => $preflightdata,
970         );
971         $params = self::validate_parameters(self::get_attempt_data_parameters(), $params);
973         list($attemptobj, $messages) = self::validate_attempt($params);
975         if ($attemptobj->is_last_page($params['page'])) {
976             $nextpage = -1;
977         } else {
978             $nextpage = $params['page'] + 1;
979         }
981         $result = array();
982         $result['attempt'] = $attemptobj->get_attempt();
983         $result['messages'] = $messages;
984         $result['nextpage'] = $nextpage;
985         $result['warnings'] = $warnings;
986         $result['questions'] = self::get_attempt_questions_data($attemptobj, false, $params['page']);
988         return $result;
989     }
991     /**
992      * Describes the get_attempt_data return value.
993      *
994      * @return external_single_structure
995      * @since Moodle 3.1
996      */
997     public static function get_attempt_data_returns() {
998         return new external_single_structure(
999             array(
1000                 'attempt' => self::attempt_structure(),
1001                 'messages' => new external_multiple_structure(
1002                     new external_value(PARAM_TEXT, 'access message'),
1003                     'access messages, will only be returned for users with mod/quiz:preview capability,
1004                     for other users this method will throw an exception if there are messages'),
1005                 'nextpage' => new external_value(PARAM_INT, 'next page number'),
1006                 'questions' => new external_multiple_structure(self::question_structure()),
1007                 'warnings' => new external_warnings(),
1008             )
1009         );
1010     }
1012     /**
1013      * Describes the parameters for get_attempt_summary.
1014      *
1015      * @return external_external_function_parameters
1016      * @since Moodle 3.1
1017      */
1018     public static function get_attempt_summary_parameters() {
1019         return new external_function_parameters (
1020             array(
1021                 'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1022                 'preflightdata' => new external_multiple_structure(
1023                     new external_single_structure(
1024                         array(
1025                             'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
1026                             'value' => new external_value(PARAM_RAW, 'data value'),
1027                         )
1028                     ), 'Preflight required data (like passwords)', VALUE_DEFAULT, array()
1029                 )
1030             )
1031         );
1032     }
1034     /**
1035      * Returns a summary of a quiz attempt before it is submitted.
1036      *
1037      * @param int $attemptid attempt id
1038      * @param int $preflightdata preflight required data (like passwords)
1039      * @return array of warnings and the attempt summary data for each question
1040      * @since Moodle 3.1
1041      */
1042     public static function get_attempt_summary($attemptid, $preflightdata = array()) {
1044         $warnings = array();
1046         $params = array(
1047             'attemptid' => $attemptid,
1048             'preflightdata' => $preflightdata,
1049         );
1050         $params = self::validate_parameters(self::get_attempt_summary_parameters(), $params);
1052         list($attemptobj, $messages) = self::validate_attempt($params, true, false);
1054         $result = array();
1055         $result['warnings'] = $warnings;
1056         $result['questions'] = self::get_attempt_questions_data($attemptobj, false, 'all');
1058         return $result;
1059     }
1061     /**
1062      * Describes the get_attempt_summary return value.
1063      *
1064      * @return external_single_structure
1065      * @since Moodle 3.1
1066      */
1067     public static function get_attempt_summary_returns() {
1068         return new external_single_structure(
1069             array(
1070                 'questions' => new external_multiple_structure(self::question_structure()),
1071                 'warnings' => new external_warnings(),
1072             )
1073         );
1074     }
1076     /**
1077      * Describes the parameters for save_attempt.
1078      *
1079      * @return external_external_function_parameters
1080      * @since Moodle 3.1
1081      */
1082     public static function save_attempt_parameters() {
1083         return new external_function_parameters (
1084             array(
1085                 'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1086                 'data' => new external_multiple_structure(
1087                     new external_single_structure(
1088                         array(
1089                             'name' => new external_value(PARAM_RAW, 'data name'),
1090                             'value' => new external_value(PARAM_RAW, 'data value'),
1091                         )
1092                     ), 'the data to be saved'
1093                 ),
1094                 'preflightdata' => new external_multiple_structure(
1095                     new external_single_structure(
1096                         array(
1097                             'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
1098                             'value' => new external_value(PARAM_RAW, 'data value'),
1099                         )
1100                     ), 'Preflight required data (like passwords)', VALUE_DEFAULT, array()
1101                 )
1102             )
1103         );
1104     }
1106     /**
1107      * Processes save requests during the quiz. This function is intended for the quiz auto-save feature.
1108      *
1109      * @param int $attemptid attempt id
1110      * @param array $data the data to be saved
1111      * @param  array $preflightdata preflight required data (like passwords)
1112      * @return array of warnings and execution result
1113      * @since Moodle 3.1
1114      */
1115     public static function save_attempt($attemptid, $data, $preflightdata = array()) {
1116         global $DB;
1118         $warnings = array();
1120         $params = array(
1121             'attemptid' => $attemptid,
1122             'data' => $data,
1123             'preflightdata' => $preflightdata,
1124         );
1125         $params = self::validate_parameters(self::save_attempt_parameters(), $params);
1127         // Add a page, required by validate_attempt.
1128         list($attemptobj, $messages) = self::validate_attempt($params);
1130         $transaction = $DB->start_delegated_transaction();
1131         // Create the $_POST object required by the question engine.
1132         $_POST = array();
1133         foreach ($data as $element) {
1134             $_POST[$element['name']] = $element['value'];
1135         }
1136         $timenow = time();
1137         $attemptobj->process_auto_save($timenow);
1138         $transaction->allow_commit();
1140         $result = array();
1141         $result['status'] = true;
1142         $result['warnings'] = $warnings;
1143         return $result;
1144     }
1146     /**
1147      * Describes the save_attempt return value.
1148      *
1149      * @return external_single_structure
1150      * @since Moodle 3.1
1151      */
1152     public static function save_attempt_returns() {
1153         return new external_single_structure(
1154             array(
1155                 'status' => new external_value(PARAM_BOOL, 'status: true if success'),
1156                 'warnings' => new external_warnings(),
1157             )
1158         );
1159     }
1161     /**
1162      * Describes the parameters for process_attempt.
1163      *
1164      * @return external_external_function_parameters
1165      * @since Moodle 3.1
1166      */
1167     public static function process_attempt_parameters() {
1168         return new external_function_parameters (
1169             array(
1170                 'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1171                 'data' => new external_multiple_structure(
1172                     new external_single_structure(
1173                         array(
1174                             'name' => new external_value(PARAM_RAW, 'data name'),
1175                             'value' => new external_value(PARAM_RAW, 'data value'),
1176                         )
1177                     ),
1178                     'the data to be saved', VALUE_DEFAULT, array()
1179                 ),
1180                 'finishattempt' => new external_value(PARAM_BOOL, 'whether to finish or not the attempt', VALUE_DEFAULT, false),
1181                 'timeup' => new external_value(PARAM_BOOL, 'whether the WS was called by a timer when the time is up',
1182                                                 VALUE_DEFAULT, false),
1183                 'preflightdata' => new external_multiple_structure(
1184                     new external_single_structure(
1185                         array(
1186                             'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
1187                             'value' => new external_value(PARAM_RAW, 'data value'),
1188                         )
1189                     ), 'Preflight required data (like passwords)', VALUE_DEFAULT, array()
1190                 )
1191             )
1192         );
1193     }
1195     /**
1196      * Process responses during an attempt at a quiz and also deals with attempts finishing.
1197      *
1198      * @param int $attemptid attempt id
1199      * @param array $data the data to be saved
1200      * @param bool $finishattempt whether to finish or not the attempt
1201      * @param bool $timeup whether the WS was called by a timer when the time is up
1202      * @param array $preflightdata preflight required data (like passwords)
1203      * @return array of warnings and the attempt state after the processing
1204      * @since Moodle 3.1
1205      */
1206     public static function process_attempt($attemptid, $data, $finishattempt = false, $timeup = false, $preflightdata = array()) {
1208         $warnings = array();
1210         $params = array(
1211             'attemptid' => $attemptid,
1212             'data' => $data,
1213             'finishattempt' => $finishattempt,
1214             'timeup' => $timeup,
1215             'preflightdata' => $preflightdata,
1216         );
1217         $params = self::validate_parameters(self::process_attempt_parameters(), $params);
1219         // Do not check access manager rules.
1220         list($attemptobj, $messages) = self::validate_attempt($params, false);
1222         // Create the $_POST object required by the question engine.
1223         $_POST = array();
1224         foreach ($params['data'] as $element) {
1225             $_POST[$element['name']] = $element['value'];
1226         }
1227         $timenow = time();
1228         $finishattempt = $params['finishattempt'];
1229         $timeup = $params['timeup'];
1231         $result = array();
1232         $result['state'] = $attemptobj->process_attempt($timenow, $finishattempt, $timeup, 0);
1233         $result['warnings'] = $warnings;
1234         return $result;
1235     }
1237     /**
1238      * Describes the process_attempt return value.
1239      *
1240      * @return external_single_structure
1241      * @since Moodle 3.1
1242      */
1243     public static function process_attempt_returns() {
1244         return new external_single_structure(
1245             array(
1246                 'state' => new external_value(PARAM_ALPHANUMEXT, 'state: the new attempt state:
1247                                                                     inprogress, finished, overdue, abandoned'),
1248                 'warnings' => new external_warnings(),
1249             )
1250         );
1251     }
1253     /**
1254      * Validate an attempt finished for review. The attempt would be reviewed by a user or a teacher.
1255      *
1256      * @param  array $params Array of parameters including the attemptid
1257      * @return  array containing the attempt object and display options
1258      * @since  Moodle 3.1
1259      * @throws  moodle_exception
1260      * @throws  moodle_quiz_exception
1261      */
1262     protected static function validate_attempt_review($params) {
1264         $attemptobj = quiz_attempt::create($params['attemptid']);
1265         $attemptobj->check_review_capability();
1267         $displayoptions = $attemptobj->get_display_options(true);
1268         if ($attemptobj->is_own_attempt()) {
1269             if (!$attemptobj->is_finished()) {
1270                 throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'attemptclosed');
1271             } else if (!$displayoptions->attempt) {
1272                 throw new moodle_exception($attemptobj->cannot_review_message());
1273             }
1274         } else if (!$attemptobj->is_review_allowed()) {
1275             throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'noreviewattempt');
1276         }
1277         return array($attemptobj, $displayoptions);
1278     }
1280     /**
1281      * Describes the parameters for get_attempt_review.
1282      *
1283      * @return external_external_function_parameters
1284      * @since Moodle 3.1
1285      */
1286     public static function get_attempt_review_parameters() {
1287         return new external_function_parameters (
1288             array(
1289                 'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1290                 'page' => new external_value(PARAM_INT, 'page number, empty for all the questions in all the pages',
1291                                                 VALUE_DEFAULT, -1),
1292             )
1293         );
1294     }
1296     /**
1297      * Returns review information for the given finished attempt, can be used by users or teachers.
1298      *
1299      * @param int $attemptid attempt id
1300      * @param int $page page number, empty for all the questions in all the pages
1301      * @return array of warnings and the attempt data, feedback and questions
1302      * @since Moodle 3.1
1303      * @throws  moodle_exception
1304      * @throws  moodle_quiz_exception
1305      */
1306     public static function get_attempt_review($attemptid, $page = -1) {
1307         global $PAGE;
1309         $warnings = array();
1311         $params = array(
1312             'attemptid' => $attemptid,
1313             'page' => $page,
1314         );
1315         $params = self::validate_parameters(self::get_attempt_review_parameters(), $params);
1317         list($attemptobj, $displayoptions) = self::validate_attempt_review($params);
1319         if ($params['page'] !== -1) {
1320             $page = $attemptobj->force_page_number_into_range($params['page']);
1321         } else {
1322             $page = 'all';
1323         }
1325         // Prepare the output.
1326         $result = array();
1327         $result['attempt'] = $attemptobj->get_attempt();
1328         $result['questions'] = self::get_attempt_questions_data($attemptobj, true, $page, true);
1330         $result['additionaldata'] = array();
1331         // Summary data (from behaviours).
1332         $summarydata = $attemptobj->get_additional_summary_data($displayoptions);
1333         foreach ($summarydata as $key => $data) {
1334             // This text does not need formatting (no need for external_format_[string|text]).
1335             $result['additionaldata'][] = array(
1336                 'id' => $key,
1337                 'title' => $data['title'], $attemptobj->get_quizobj()->get_context()->id,
1338                 'content' => $data['content'],
1339             );
1340         }
1342         // Feedback if there is any, and the user is allowed to see it now.
1343         $grade = quiz_rescale_grade($attemptobj->get_attempt()->sumgrades, $attemptobj->get_quiz(), false);
1345         $feedback = $attemptobj->get_overall_feedback($grade);
1346         if ($displayoptions->overallfeedback && $feedback) {
1347             $result['additionaldata'][] = array(
1348                 'id' => 'feedback',
1349                 'title' => get_string('feedback', 'quiz'),
1350                 'content' => $feedback,
1351             );
1352         }
1354         $result['grade'] = $grade;
1355         $result['warnings'] = $warnings;
1356         return $result;
1357     }
1359     /**
1360      * Describes the get_attempt_review return value.
1361      *
1362      * @return external_single_structure
1363      * @since Moodle 3.1
1364      */
1365     public static function get_attempt_review_returns() {
1366         return new external_single_structure(
1367             array(
1368                 'grade' => new external_value(PARAM_RAW, 'grade for the quiz (or empty or "notyetgraded")'),
1369                 'attempt' => self::attempt_structure(),
1370                 'additionaldata' => new external_multiple_structure(
1371                     new external_single_structure(
1372                         array(
1373                             'id' => new external_value(PARAM_ALPHANUMEXT, 'id of the data'),
1374                             'title' => new external_value(PARAM_TEXT, 'data title'),
1375                             'content' => new external_value(PARAM_RAW, 'data content'),
1376                         )
1377                     )
1378                 ),
1379                 'questions' => new external_multiple_structure(self::question_structure()),
1380                 'warnings' => new external_warnings(),
1381             )
1382         );
1383     }
1385     /**
1386      * Describes the parameters for view_attempt.
1387      *
1388      * @return external_external_function_parameters
1389      * @since Moodle 3.1
1390      */
1391     public static function view_attempt_parameters() {
1392         return new external_function_parameters (
1393             array(
1394                 'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1395                 'page' => new external_value(PARAM_INT, 'page number'),
1396             )
1397         );
1398     }
1400     /**
1401      * Trigger the attempt viewed event.
1402      *
1403      * @param int $attemptid attempt id
1404      * @param int $page page number
1405      * @return array of warnings and status result
1406      * @since Moodle 3.1
1407      */
1408     public static function view_attempt($attemptid, $page) {
1410         $warnings = array();
1412         $params = array(
1413             'attemptid' => $attemptid,
1414             'page' => $page,
1415         );
1416         $params = self::validate_parameters(self::view_attempt_parameters(), $params);
1417         list($attemptobj, $messages) = self::validate_attempt($params);
1419         // Log action.
1420         $attemptobj->fire_attempt_viewed_event();
1422         // Update attempt page, throwing an exception if $page is not valid.
1423         if (!$attemptobj->set_currentpage($params['page'])) {
1424             throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'Out of sequence access');
1425         }
1427         $result = array();
1428         $result['status'] = true;
1429         $result['warnings'] = $warnings;
1430         return $result;
1431     }
1433     /**
1434      * Describes the view_attempt return value.
1435      *
1436      * @return external_single_structure
1437      * @since Moodle 3.1
1438      */
1439     public static function view_attempt_returns() {
1440         return new external_single_structure(
1441             array(
1442                 'status' => new external_value(PARAM_BOOL, 'status: true if success'),
1443                 'warnings' => new external_warnings(),
1444             )
1445         );
1446     }
1448     /**
1449      * Describes the parameters for view_attempt_summary.
1450      *
1451      * @return external_external_function_parameters
1452      * @since Moodle 3.1
1453      */
1454     public static function view_attempt_summary_parameters() {
1455         return new external_function_parameters (
1456             array(
1457                 'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1458             )
1459         );
1460     }
1462     /**
1463      * Trigger the attempt summary viewed event.
1464      *
1465      * @param int $attemptid attempt id
1466      * @return array of warnings and status result
1467      * @since Moodle 3.1
1468      */
1469     public static function view_attempt_summary($attemptid) {
1471         $warnings = array();
1473         $params = array(
1474             'attemptid' => $attemptid,
1475         );
1476         $params = self::validate_parameters(self::view_attempt_summary_parameters(), $params);
1477         list($attemptobj, $messages) = self::validate_attempt($params);
1479         // Log action.
1480         $attemptobj->fire_attempt_summary_viewed_event();
1482         $result = array();
1483         $result['status'] = true;
1484         $result['warnings'] = $warnings;
1485         return $result;
1486     }
1488     /**
1489      * Describes the view_attempt_summary return value.
1490      *
1491      * @return external_single_structure
1492      * @since Moodle 3.1
1493      */
1494     public static function view_attempt_summary_returns() {
1495         return new external_single_structure(
1496             array(
1497                 'status' => new external_value(PARAM_BOOL, 'status: true if success'),
1498                 'warnings' => new external_warnings(),
1499             )
1500         );
1501     }
1503     /**
1504      * Describes the parameters for view_attempt_review.
1505      *
1506      * @return external_external_function_parameters
1507      * @since Moodle 3.1
1508      */
1509     public static function view_attempt_review_parameters() {
1510         return new external_function_parameters (
1511             array(
1512                 'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1513             )
1514         );
1515     }
1517     /**
1518      * Trigger the attempt reviewed event.
1519      *
1520      * @param int $attemptid attempt id
1521      * @return array of warnings and status result
1522      * @since Moodle 3.1
1523      */
1524     public static function view_attempt_review($attemptid) {
1526         $warnings = array();
1528         $params = array(
1529             'attemptid' => $attemptid,
1530         );
1531         $params = self::validate_parameters(self::view_attempt_review_parameters(), $params);
1532         list($attemptobj, $displayoptions) = self::validate_attempt_review($params);
1534         // Log action.
1535         $attemptobj->fire_attempt_reviewed_event();
1537         $result = array();
1538         $result['status'] = true;
1539         $result['warnings'] = $warnings;
1540         return $result;
1541     }
1543     /**
1544      * Describes the view_attempt_review return value.
1545      *
1546      * @return external_single_structure
1547      * @since Moodle 3.1
1548      */
1549     public static function view_attempt_review_returns() {
1550         return new external_single_structure(
1551             array(
1552                 'status' => new external_value(PARAM_BOOL, 'status: true if success'),
1553                 'warnings' => new external_warnings(),
1554             )
1555         );
1556     }
1558     /**
1559      * Describes the parameters for view_quiz.
1560      *
1561      * @return external_external_function_parameters
1562      * @since Moodle 3.1
1563      */
1564     public static function get_quiz_feedback_for_grade_parameters() {
1565         return new external_function_parameters (
1566             array(
1567                 'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
1568                 'grade' => new external_value(PARAM_FLOAT, 'the grade to check'),
1569             )
1570         );
1571     }
1573     /**
1574      * Get the feedback text that should be show to a student who got the given grade in the given quiz.
1575      *
1576      * @param int $quizid quiz instance id
1577      * @param float $grade the grade to check
1578      * @return array of warnings and status result
1579      * @since Moodle 3.1
1580      * @throws moodle_exception
1581      */
1582     public static function get_quiz_feedback_for_grade($quizid, $grade) {
1583         global $DB;
1585         $params = array(
1586             'quizid' => $quizid,
1587             'grade' => $grade,
1588         );
1589         $params = self::validate_parameters(self::get_quiz_feedback_for_grade_parameters(), $params);
1590         $warnings = array();
1592         // Request and permission validation.
1593         $quiz = $DB->get_record('quiz', array('id' => $params['quizid']), '*', MUST_EXIST);
1594         list($course, $cm) = get_course_and_cm_from_instance($quiz, 'quiz');
1596         $context = context_module::instance($cm->id);
1597         self::validate_context($context);
1599         $result = array();
1600         $result['feedbacktext'] = '';
1601         $result['feedbacktextformat'] = FORMAT_MOODLE;
1603         $feedback = quiz_feedback_record_for_grade($params['grade'], $quiz);
1604         if (!empty($feedback->feedbacktext)) {
1605             list($text, $format) = external_format_text($feedback->feedbacktext, $feedback->feedbacktextformat, $context->id,
1606                                                         'mod_quiz', 'feedback', $feedback->id);
1607             $result['feedbacktext'] = $text;
1608             $result['feedbacktextformat'] = $format;
1609         }
1611         $result['warnings'] = $warnings;
1612         return $result;
1613     }
1615     /**
1616      * Describes the get_quiz_feedback_for_grade return value.
1617      *
1618      * @return external_single_structure
1619      * @since Moodle 3.1
1620      */
1621     public static function get_quiz_feedback_for_grade_returns() {
1622         return new external_single_structure(
1623             array(
1624                 'feedbacktext' => new external_value(PARAM_RAW, 'the comment that corresponds to this grade (empty for none)'),
1625                 'feedbacktextformat' => new external_format_value('feedbacktext', VALUE_OPTIONAL),
1626                 'warnings' => new external_warnings(),
1627             )
1628         );
1629     }