Merge branch 'MDL-53034-master' of git://github.com/jleyva/moodle
[moodle.git] / mod / quiz / classes / external.php
CommitLineData
51e27aac
JL
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/>.
16
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 */
26
27defined('MOODLE_INTERNAL') || die;
28
29require_once($CFG->libdir . '/externallib.php');
30require_once($CFG->dirroot . '/mod/quiz/locallib.php');
31
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 */
41class mod_quiz_external extends external_api {
42
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 }
58
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;
69
70 $warnings = array();
71 $returnedquizzes = array();
72
73 $params = array(
74 'courseids' => $courseids,
75 );
76 $params = self::validate_parameters(self::get_quizzes_by_courses_parameters(), $params);
77
78 $mycourses = array();
79 if (empty($params['courseids'])) {
80 $mycourses = enrol_get_my_courses();
81 $params['courseids'] = array_keys($mycourses);
82 }
83
84 // Ensure there are courseids to loop through.
85 if (!empty($params['courseids'])) {
86
87 list($courses, $warnings) = external_util::validate_courses($params['courseids'], $mycourses);
88
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);
94
95 // Update quiz with override information.
96 $quiz = quiz_update_effective_access($quiz, $USER->id);
97
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);
105
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);
110
111 $viewablefields = array('timeopen', 'timeclose', 'grademethod', 'section', 'visible', 'groupmode',
112 'groupingid');
113
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));
118
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');
126
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 }
136
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 }
142
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 }
155
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),
21d43c12 192 'attemptonlast' => new external_value(PARAM_INT, 'Whether subsequent attempts start from the answer
51e27aac
JL
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),
21d43c12 258 'completionpass' => new external_value(PARAM_INT, 'Whether to require passing grade', VALUE_OPTIONAL),
51e27aac
JL
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 }
274
4064dd0e
JL
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 }
288
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;
299
300 $params = self::validate_parameters(self::view_quiz_parameters(), array('quizid' => $quizid));
301 $warnings = array();
302
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');
306
307 $context = context_module::instance($cm->id);
308 self::validate_context($context);
309
310 // Trigger course_module_viewed event and completion.
311 quiz_view($quiz, $course, $cm, $context);
312
313 $result = array();
314 $result['status'] = true;
315 $result['warnings'] = $warnings;
316 return $result;
317 }
318
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 }
333
c161ecff
JL
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),
347
348 )
349 );
350 }
351
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;
365
366 $warnings = array();
367
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);
375
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');
379
380 $context = context_module::instance($cm->id);
381 self::validate_context($context);
382
383 if (!in_array($params['status'], array('all', 'finished', 'unfinished'))) {
384 throw new invalid_parameter_exception('Invalid status value');
385 }
386
387 // Default value for userid.
388 if (empty($params['userid'])) {
389 $params['userid'] = $USER->id;
390 }
391
392 $user = core_user::get_user($params['userid'], '*', MUST_EXIST);
393 core_user::require_active_user($user);
394
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 }
399
400 $attempts = quiz_get_user_attempts($quiz->id, $user->id, $params['status'], $params['includepreviews']);
401
402 $result = array();
403 $result['attempts'] = $attempts;
404 $result['warnings'] = $warnings;
405 return $result;
406 }
407
b8954440
JL
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 }
441
c161ecff
JL
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(
b8954440 451 'attempts' => new external_multiple_structure(self::attempt_structure()),
c161ecff
JL
452 'warnings' => new external_warnings(),
453 )
454 );
455 }
456
e73e4581
JL
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 }
471
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;
482
483 $warnings = array();
484
485 $params = array(
486 'quizid' => $quizid,
487 'userid' => $userid,
488 );
489 $params = self::validate_parameters(self::get_user_best_grade_parameters(), $params);
490
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');
494
495 $context = context_module::instance($cm->id);
496 self::validate_context($context);
497
498 // Default value for userid.
499 if (empty($params['userid'])) {
500 $params['userid'] = $USER->id;
501 }
502
503 $user = core_user::get_user($params['userid'], '*', MUST_EXIST);
504 core_user::require_active_user($user);
505
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 }
510
511 $result = array();
512 $grade = quiz_get_best_grade($quiz, $user->id);
513
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 }
523
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 }
539
1f67c0b8
JL
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),
551
552 )
553 );
554 }
555
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;
566
567 $warnings = array();
568
569 $params = array(
570 'quizid' => $quizid,
571 'userid' => $userid,
572 );
573 $params = self::validate_parameters(self::get_combined_review_options_parameters(), $params);
574
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');
578
579 $context = context_module::instance($cm->id);
580 self::validate_context($context);
581
582 // Default value for userid.
583 if (empty($params['userid'])) {
584 $params['userid'] = $USER->id;
585 }
586
587 $user = core_user::get_user($params['userid'], '*', MUST_EXIST);
588 core_user::require_active_user($user);
589
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 }
594
595 $attempts = quiz_get_user_attempts($quiz->id, $user->id, 'all', true);
596
597 $result = array();
598 $result['someoptions'] = [];
599 $result['alloptions'] = [];
600
601 list($someoptions, $alloptions) = quiz_get_combined_reviewoptions($quiz, $attempts);
602
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 }
611
612 $result['warnings'] = $warnings;
613 return $result;
614 }
615
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 }
645
b8954440
JL
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),
665
666 )
667 );
668 }
669
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;
682
683 $warnings = array();
684 $attempt = array();
685
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'];
693
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');
697
698 $context = context_module::instance($cm->id);
699 self::validate_context($context);
700
701 $quizobj = quiz::create($cm->instance, $USER->id);
702
703 // Check questions.
704 if (!$quizobj->has_questions()) {
705 throw new moodle_quiz_exception($quizobj, 'noquestionsfound');
706 }
707
708 // Create an object to manage all the other (non-roles) access rules.
709 $timenow = time();
710 $accessmanager = $quizobj->get_access_manager($timenow);
711
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);
715
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.
730
731 $provideddata = array();
732 foreach ($params['preflightdata'] as $data) {
733 $provideddata[$data['name']] = $data['value'];
734 }
735
736 $errors = $accessmanager->validate_preflight_check($provideddata, [], $currentattemptid);
737
738 if (!empty($errors)) {
739 throw new moodle_quiz_exception($quizobj, array_shift($errors));
740 }
741
742 // Pre-flight check passed.
743 $accessmanager->notify_preflight_check_passed($currentattemptid);
744 }
745
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 }
755
756 $result = array();
757 $result['attempt'] = $attempt;
758 $result['warnings'] = $warnings;
759 return $result;
760 }
761
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 }
776
bc247b0d
JL
777 /**
778 * Utility function for validating a given attempt
779 *
98e68690
JL
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
bc247b0d
JL
783 * @return array containing the attempt object and access messages
784 * @throws moodle_quiz_exception
785 * @since Moodle 3.1
786 */
98e68690 787 protected static function validate_attempt($params, $checkaccessrules = true, $failifoverdue = true) {
bc247b0d
JL
788 global $USER;
789
790 $attemptobj = quiz_attempt::create($params['attemptid']);
791
792 $context = context_module::instance($attemptobj->get_cm()->id);
793 self::validate_context($context);
794
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 }
799
800 // General capabilities check.
801 $ispreviewuser = $attemptobj->is_preview_user();
802 if (!$ispreviewuser) {
803 $attemptobj->require_capability('mod/quiz:attempt');
804 }
805
98e68690
JL
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);
812
813 $messages = $accessmanager->prevent_access();
814 if (!$ispreviewuser && $messages) {
815 throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'attempterror');
816 }
817 }
818
bc247b0d
JL
819 // Attempt closed?.
820 if ($attemptobj->is_finished()) {
821 throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'attemptalreadyclosed');
98e68690 822 } else if ($failifoverdue && $attemptobj->get_state() == quiz_attempt::OVERDUE) {
bc247b0d
JL
823 throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'stateoverdue');
824 }
825
bc247b0d
JL
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 }
832
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 }
840
bc9733e7
JL
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 }
846
847 // Prevent out of sequence access.
899983ee
JL
848 if (!$attemptobj->check_page_access($params['page'])) {
849 throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'Out of sequence access');
bc247b0d 850 }
bc247b0d 851
bc9733e7
JL
852 // Check slots.
853 $slots = $attemptobj->get_slots($params['page']);
bc247b0d 854
bc9733e7
JL
855 if (empty($slots)) {
856 throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'noquestionsfound');
857 }
bc247b0d
JL
858 }
859
860 return array($attemptobj, $messages);
861 }
862
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 }
885
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;
896
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');
901
902 foreach ($attemptobj->get_slots($page) as $slot) {
903
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 );
911
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 }
923
924 $questions[] = $question;
925 }
926 return $questions;
927 }
928
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 }
951
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()) {
963
964 $warnings = array();
965
966 $params = array(
967 'attemptid' => $attemptid,
968 'page' => $page,
969 'preflightdata' => $preflightdata,
970 );
971 $params = self::validate_parameters(self::get_attempt_data_parameters(), $params);
972
973 list($attemptobj, $messages) = self::validate_attempt($params);
974
975 if ($attemptobj->is_last_page($params['page'])) {
976 $nextpage = -1;
977 } else {
978 $nextpage = $params['page'] + 1;
979 }
980
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']);
987
988 return $result;
989 }
990
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 }
1011
bc9733e7
JL
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 }
1033
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()) {
1043
1044 $warnings = array();
1045
1046 $params = array(
1047 'attemptid' => $attemptid,
1048 'preflightdata' => $preflightdata,
1049 );
1050 $params = self::validate_parameters(self::get_attempt_summary_parameters(), $params);
1051
98e68690 1052 list($attemptobj, $messages) = self::validate_attempt($params, true, false);
bc9733e7
JL
1053
1054 $result = array();
1055 $result['warnings'] = $warnings;
1056 $result['questions'] = self::get_attempt_questions_data($attemptobj, false, 'all');
1057
1058 return $result;
1059 }
1060
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 }
1075
96d5607c
JL
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 }
1105
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;
1117
1118 $warnings = array();
1119
1120 $params = array(
1121 'attemptid' => $attemptid,
1122 'data' => $data,
1123 'preflightdata' => $preflightdata,
1124 );
1125 $params = self::validate_parameters(self::save_attempt_parameters(), $params);
1126
1127 // Add a page, required by validate_attempt.
1128 list($attemptobj, $messages) = self::validate_attempt($params);
1129
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();
1139
1140 $result = array();
1141 $result['status'] = true;
1142 $result['warnings'] = $warnings;
1143 return $result;
1144 }
1145
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 }
1160
98e68690
JL
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 }
1194
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()) {
1207
1208 $warnings = array();
1209
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);
1218
1219 // Do not check access manager rules.
1220 list($attemptobj, $messages) = self::validate_attempt($params, false);
1221
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'];
1230
1231 $result = array();
1232 $result['state'] = $attemptobj->process_attempt($timenow, $finishattempt, $timeup, 0);
1233 $result['warnings'] = $warnings;
1234 return $result;
1235 }
1236
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 }
1252
3589b659
JL
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) {
1263
1264 $attemptobj = quiz_attempt::create($params['attemptid']);
1265 $attemptobj->check_review_capability();
1266
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 }
1279
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 }
1295
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;
1308
1309 $warnings = array();
1310
1311 $params = array(
1312 'attemptid' => $attemptid,
1313 'page' => $page,
1314 );
1315 $params = self::validate_parameters(self::get_attempt_review_parameters(), $params);
1316
1317 list($attemptobj, $displayoptions) = self::validate_attempt_review($params);
1318
1319 if ($params['page'] !== -1) {
1320 $page = $attemptobj->force_page_number_into_range($params['page']);
1321 } else {
1322 $page = 'all';
1323 }
1324
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);
1329
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 }
1341
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);
1344
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 }
1353
1354 $result['grade'] = $grade;
1355 $result['warnings'] = $warnings;
1356 return $result;
1357 }
1358
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 }
1384
899983ee
JL
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 }
1399
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) {
1409
1410 $warnings = array();
1411
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);
1418
1419 // Log action.
1420 $attemptobj->fire_attempt_viewed_event();
1421
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 }
1426
1427 $result = array();
1428 $result['status'] = true;
1429 $result['warnings'] = $warnings;
1430 return $result;
1431 }
1432
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 }
1447
d9ef6ae0
JL
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 }
1461
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) {
1470
1471 $warnings = array();
1472
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);
1478
1479 // Log action.
1480 $attemptobj->fire_attempt_summary_viewed_event();
1481
1482 $result = array();
1483 $result['status'] = true;
1484 $result['warnings'] = $warnings;
1485 return $result;
1486 }
1487
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 }
1502
3e5c19a0
JL
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 }
1516
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) {
1525
1526 $warnings = array();
1527
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);
1533
1534 // Log action.
1535 $attemptobj->fire_attempt_reviewed_event();
1536
1537 $result = array();
1538 $result['status'] = true;
1539 $result['warnings'] = $warnings;
1540 return $result;
1541 }
1542
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 }
1557
48abca79
JL
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 }
1572
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;
1584
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();
1591
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');
1595
1596 $context = context_module::instance($cm->id);
1597 self::validate_context($context);
1598
1599 $result = array();
1600 $result['feedbacktext'] = '';
1601 $result['feedbacktextformat'] = FORMAT_MOODLE;
1602
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 }
1610
1611 $result['warnings'] = $warnings;
1612 return $result;
1613 }
1614
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 }
1630
51e27aac 1631}