MDL-69823 mod_quiz: Return question options via WS
[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 *
9db43c73 46 * @return external_function_parameters
51e27aac
JL
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.
dfd48815
JL
108 $options = array('noclean' => true);
109 list($quizdetails['intro'], $quizdetails['introformat']) =
110 external_format_text($quiz->intro, $quiz->introformat, $context->id, 'mod_quiz', 'intro', null, $options);
51e27aac 111
7ef49bd3 112 $quizdetails['introfiles'] = external_util::get_area_files($context->id, 'mod_quiz', 'intro', false, false);
51e27aac 113 $viewablefields = array('timeopen', 'timeclose', 'grademethod', 'section', 'visible', 'groupmode',
bcd6249d
JL
114 'groupingid', 'attempts', 'timelimit', 'grademethod', 'decimalpoints',
115 'questiondecimalpoints', 'sumgrades', 'grade', 'preferredbehaviour');
116 // Some times this function returns just empty.
117 $hasfeedback = quiz_has_feedback($quiz);
118 $quizdetails['hasfeedback'] = (!empty($hasfeedback)) ? 1 : 0;
51e27aac
JL
119
120 $timenow = time();
121 $quizobj = quiz::create($quiz->id, $USER->id);
122 $accessmanager = new quiz_access_manager($quizobj, $timenow, has_capability('mod/quiz:ignoretimelimits',
123 $context, null, false));
124
125 // Fields the user could see if have access to the quiz.
126 if (!$accessmanager->prevent_access()) {
51e27aac
JL
127 $quizdetails['hasquestions'] = (int) $quizobj->has_questions();
128 $quizdetails['autosaveperiod'] = get_config('quiz', 'autosaveperiod');
129
bcd6249d 130 $additionalfields = array('attemptonlast', 'reviewattempt', 'reviewcorrectness', 'reviewmarks',
51e27aac 131 'reviewspecificfeedback', 'reviewgeneralfeedback', 'reviewrightanswer',
bcd6249d 132 'reviewoverallfeedback', 'questionsperpage', 'navmethod',
51e27aac
JL
133 'browsersecurity', 'delay1', 'delay2', 'showuserpicture', 'showblocks',
134 'completionattemptsexhausted', 'completionpass', 'overduehandling',
bcd6249d 135 'graceperiod', 'canredoquestions', 'allowofflineattempts');
51e27aac
JL
136 $viewablefields = array_merge($viewablefields, $additionalfields);
137 }
138
139 // Fields only for managers.
140 if (has_capability('moodle/course:manageactivities', $context)) {
141 $additionalfields = array('shuffleanswers', 'timecreated', 'timemodified', 'password', 'subnet');
142 $viewablefields = array_merge($viewablefields, $additionalfields);
143 }
144
145 foreach ($viewablefields as $field) {
146 $quizdetails[$field] = $quiz->{$field};
147 }
148 }
149 $returnedquizzes[] = $quizdetails;
150 }
151 }
152 $result = array();
153 $result['quizzes'] = $returnedquizzes;
154 $result['warnings'] = $warnings;
155 return $result;
156 }
157
158 /**
159 * Describes the get_quizzes_by_courses return value.
160 *
161 * @return external_single_structure
162 * @since Moodle 3.1
163 */
164 public static function get_quizzes_by_courses_returns() {
165 return new external_single_structure(
166 array(
167 'quizzes' => new external_multiple_structure(
168 new external_single_structure(
169 array(
170 'id' => new external_value(PARAM_INT, 'Standard Moodle primary key.'),
171 'course' => new external_value(PARAM_INT, 'Foreign key reference to the course this quiz is part of.'),
172 'coursemodule' => new external_value(PARAM_INT, 'Course module id.'),
173 'name' => new external_value(PARAM_RAW, 'Quiz name.'),
174 'intro' => new external_value(PARAM_RAW, 'Quiz introduction text.', VALUE_OPTIONAL),
175 'introformat' => new external_format_value('intro', VALUE_OPTIONAL),
7ef49bd3 176 'introfiles' => new external_files('Files in the introduction text', VALUE_OPTIONAL),
51e27aac
JL
177 'timeopen' => new external_value(PARAM_INT, 'The time when this quiz opens. (0 = no restriction.)',
178 VALUE_OPTIONAL),
179 'timeclose' => new external_value(PARAM_INT, 'The time when this quiz closes. (0 = no restriction.)',
180 VALUE_OPTIONAL),
181 'timelimit' => new external_value(PARAM_INT, 'The time limit for quiz attempts, in seconds.',
182 VALUE_OPTIONAL),
183 'overduehandling' => new external_value(PARAM_ALPHA, 'The method used to handle overdue attempts.
184 \'autosubmit\', \'graceperiod\' or \'autoabandon\'.',
185 VALUE_OPTIONAL),
186 'graceperiod' => new external_value(PARAM_INT, 'The amount of time (in seconds) after the time limit
187 runs out during which attempts can still be submitted,
188 if overduehandling is set to allow it.', VALUE_OPTIONAL),
189 'preferredbehaviour' => new external_value(PARAM_ALPHANUMEXT, 'The behaviour to ask questions to use.',
190 VALUE_OPTIONAL),
191 'canredoquestions' => new external_value(PARAM_INT, 'Allows students to redo any completed question
192 within a quiz attempt.', VALUE_OPTIONAL),
193 'attempts' => new external_value(PARAM_INT, 'The maximum number of attempts a student is allowed.',
194 VALUE_OPTIONAL),
21d43c12 195 'attemptonlast' => new external_value(PARAM_INT, 'Whether subsequent attempts start from the answer
51e27aac
JL
196 to the previous attempt (1) or start blank (0).',
197 VALUE_OPTIONAL),
198 'grademethod' => new external_value(PARAM_INT, 'One of the values QUIZ_GRADEHIGHEST, QUIZ_GRADEAVERAGE,
199 QUIZ_ATTEMPTFIRST or QUIZ_ATTEMPTLAST.', VALUE_OPTIONAL),
200 'decimalpoints' => new external_value(PARAM_INT, 'Number of decimal points to use when displaying
201 grades.', VALUE_OPTIONAL),
202 'questiondecimalpoints' => new external_value(PARAM_INT, 'Number of decimal points to use when
203 displaying question grades.
204 (-1 means use decimalpoints.)', VALUE_OPTIONAL),
205 'reviewattempt' => new external_value(PARAM_INT, 'Whether users are allowed to review their quiz
206 attempts at various times. This is a bit field, decoded by the
207 mod_quiz_display_options class. It is formed by ORing together
208 the constants defined there.', VALUE_OPTIONAL),
209 'reviewcorrectness' => new external_value(PARAM_INT, 'Whether users are allowed to review their quiz
210 attempts at various times.
211 A bit field, like reviewattempt.', VALUE_OPTIONAL),
212 'reviewmarks' => new external_value(PARAM_INT, 'Whether users are allowed to review their quiz attempts
213 at various times. A bit field, like reviewattempt.',
214 VALUE_OPTIONAL),
215 'reviewspecificfeedback' => 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 'reviewgeneralfeedback' => new external_value(PARAM_INT, 'Whether users are allowed to review their
219 quiz attempts at various times. A bit field, like
220 reviewattempt.', VALUE_OPTIONAL),
221 'reviewrightanswer' => 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 'reviewoverallfeedback' => new external_value(PARAM_INT, 'Whether users are allowed to review their quiz
225 attempts at various times. A bit field, like
226 reviewattempt.', VALUE_OPTIONAL),
227 'questionsperpage' => new external_value(PARAM_INT, 'How often to insert a page break when editing
228 the quiz, or when shuffling the question order.',
229 VALUE_OPTIONAL),
230 'navmethod' => new external_value(PARAM_ALPHA, 'Any constraints on how the user is allowed to navigate
231 around the quiz. Currently recognised values are
232 \'free\' and \'seq\'.', VALUE_OPTIONAL),
233 'shuffleanswers' => new external_value(PARAM_INT, 'Whether the parts of the question should be shuffled,
234 in those question types that support it.', VALUE_OPTIONAL),
235 'sumgrades' => new external_value(PARAM_FLOAT, 'The total of all the question instance maxmarks.',
236 VALUE_OPTIONAL),
237 'grade' => new external_value(PARAM_FLOAT, 'The total that the quiz overall grade is scaled to be
238 out of.', VALUE_OPTIONAL),
239 'timecreated' => new external_value(PARAM_INT, 'The time when the quiz was added to the course.',
240 VALUE_OPTIONAL),
241 'timemodified' => new external_value(PARAM_INT, 'Last modified time.',
242 VALUE_OPTIONAL),
243 'password' => new external_value(PARAM_RAW, 'A password that the student must enter before starting or
244 continuing a quiz attempt.', VALUE_OPTIONAL),
245 'subnet' => new external_value(PARAM_RAW, 'Used to restrict the IP addresses from which this quiz can
246 be attempted. The format is as requried by the address_in_subnet
247 function.', VALUE_OPTIONAL),
248 'browsersecurity' => new external_value(PARAM_ALPHANUMEXT, 'Restriciton on the browser the student must
249 use. E.g. \'securewindow\'.', VALUE_OPTIONAL),
250 'delay1' => new external_value(PARAM_INT, 'Delay that must be left between the first and second attempt,
251 in seconds.', VALUE_OPTIONAL),
252 'delay2' => new external_value(PARAM_INT, 'Delay that must be left between the second and subsequent
253 attempt, in seconds.', VALUE_OPTIONAL),
254 'showuserpicture' => new external_value(PARAM_INT, 'Option to show the user\'s picture during the
255 attempt and on the review page.', VALUE_OPTIONAL),
256 'showblocks' => new external_value(PARAM_INT, 'Whether blocks should be shown on the attempt.php and
257 review.php pages.', VALUE_OPTIONAL),
258 'completionattemptsexhausted' => new external_value(PARAM_INT, 'Mark quiz complete when the student has
259 exhausted the maximum number of attempts',
260 VALUE_OPTIONAL),
21d43c12 261 'completionpass' => new external_value(PARAM_INT, 'Whether to require passing grade', VALUE_OPTIONAL),
8a972ab3
JL
262 'allowofflineattempts' => new external_value(PARAM_INT, 'Whether to allow the quiz to be attempted
263 offline in the mobile app', VALUE_OPTIONAL),
51e27aac
JL
264 'autosaveperiod' => new external_value(PARAM_INT, 'Auto-save delay', VALUE_OPTIONAL),
265 'hasfeedback' => new external_value(PARAM_INT, 'Whether the quiz has any non-blank feedback text',
266 VALUE_OPTIONAL),
267 'hasquestions' => new external_value(PARAM_INT, 'Whether the quiz has questions', VALUE_OPTIONAL),
268 'section' => new external_value(PARAM_INT, 'Course section id', VALUE_OPTIONAL),
269 'visible' => new external_value(PARAM_INT, 'Module visibility', VALUE_OPTIONAL),
270 'groupmode' => new external_value(PARAM_INT, 'Group mode', VALUE_OPTIONAL),
271 'groupingid' => new external_value(PARAM_INT, 'Grouping id', VALUE_OPTIONAL),
272 )
273 )
274 ),
275 'warnings' => new external_warnings(),
276 )
277 );
278 }
279
a79a6361
JL
280
281 /**
282 * Utility function for validating a quiz.
283 *
284 * @param int $quizid quiz instance id
285 * @return array array containing the quiz, course, context and course module objects
286 * @since Moodle 3.1
287 */
288 protected static function validate_quiz($quizid) {
289 global $DB;
290
291 // Request and permission validation.
292 $quiz = $DB->get_record('quiz', array('id' => $quizid), '*', MUST_EXIST);
293 list($course, $cm) = get_course_and_cm_from_instance($quiz, 'quiz');
294
295 $context = context_module::instance($cm->id);
296 self::validate_context($context);
297
298 return array($quiz, $course, $cm, $context);
299 }
300
4064dd0e
JL
301 /**
302 * Describes the parameters for view_quiz.
303 *
9db43c73 304 * @return external_function_parameters
4064dd0e
JL
305 * @since Moodle 3.1
306 */
307 public static function view_quiz_parameters() {
308 return new external_function_parameters (
309 array(
310 'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
311 )
312 );
313 }
314
315 /**
316 * Trigger the course module viewed event and update the module completion status.
317 *
318 * @param int $quizid quiz instance id
319 * @return array of warnings and status result
320 * @since Moodle 3.1
321 * @throws moodle_exception
322 */
323 public static function view_quiz($quizid) {
324 global $DB;
325
326 $params = self::validate_parameters(self::view_quiz_parameters(), array('quizid' => $quizid));
327 $warnings = array();
328
a79a6361 329 list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
4064dd0e
JL
330
331 // Trigger course_module_viewed event and completion.
332 quiz_view($quiz, $course, $cm, $context);
333
334 $result = array();
335 $result['status'] = true;
336 $result['warnings'] = $warnings;
337 return $result;
338 }
339
340 /**
341 * Describes the view_quiz return value.
342 *
343 * @return external_single_structure
344 * @since Moodle 3.1
345 */
346 public static function view_quiz_returns() {
347 return new external_single_structure(
348 array(
349 'status' => new external_value(PARAM_BOOL, 'status: true if success'),
350 'warnings' => new external_warnings(),
351 )
352 );
353 }
354
c161ecff
JL
355 /**
356 * Describes the parameters for get_user_attempts.
357 *
9db43c73 358 * @return external_function_parameters
c161ecff
JL
359 * @since Moodle 3.1
360 */
361 public static function get_user_attempts_parameters() {
362 return new external_function_parameters (
363 array(
364 'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
365 'userid' => new external_value(PARAM_INT, 'user id, empty for current user', VALUE_DEFAULT, 0),
366 'status' => new external_value(PARAM_ALPHA, 'quiz status: all, finished or unfinished', VALUE_DEFAULT, 'finished'),
367 'includepreviews' => new external_value(PARAM_BOOL, 'whether to include previews or not', VALUE_DEFAULT, false),
368
369 )
370 );
371 }
372
373 /**
374 * Return a list of attempts for the given quiz and user.
375 *
376 * @param int $quizid quiz instance id
377 * @param int $userid user id
378 * @param string $status quiz status: all, finished or unfinished
379 * @param bool $includepreviews whether to include previews or not
380 * @return array of warnings and the list of attempts
381 * @since Moodle 3.1
382 * @throws invalid_parameter_exception
383 */
384 public static function get_user_attempts($quizid, $userid = 0, $status = 'finished', $includepreviews = false) {
385 global $DB, $USER;
386
387 $warnings = array();
388
389 $params = array(
390 'quizid' => $quizid,
391 'userid' => $userid,
392 'status' => $status,
393 'includepreviews' => $includepreviews,
394 );
395 $params = self::validate_parameters(self::get_user_attempts_parameters(), $params);
396
a79a6361 397 list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
c161ecff
JL
398
399 if (!in_array($params['status'], array('all', 'finished', 'unfinished'))) {
400 throw new invalid_parameter_exception('Invalid status value');
401 }
402
403 // Default value for userid.
404 if (empty($params['userid'])) {
405 $params['userid'] = $USER->id;
406 }
407
408 $user = core_user::get_user($params['userid'], '*', MUST_EXIST);
409 core_user::require_active_user($user);
410
411 // Extra checks so only users with permissions can view other users attempts.
412 if ($USER->id != $user->id) {
413 require_capability('mod/quiz:viewreports', $context);
414 }
415
416 $attempts = quiz_get_user_attempts($quiz->id, $user->id, $params['status'], $params['includepreviews']);
417
418 $result = array();
419 $result['attempts'] = $attempts;
420 $result['warnings'] = $warnings;
421 return $result;
422 }
423
b8954440
JL
424 /**
425 * Describes a single attempt structure.
426 *
427 * @return external_single_structure the attempt structure
428 */
429 private static function attempt_structure() {
430 return new external_single_structure(
431 array(
432 'id' => new external_value(PARAM_INT, 'Attempt id.', VALUE_OPTIONAL),
433 'quiz' => new external_value(PARAM_INT, 'Foreign key reference to the quiz that was attempted.',
434 VALUE_OPTIONAL),
435 'userid' => new external_value(PARAM_INT, 'Foreign key reference to the user whose attempt this is.',
436 VALUE_OPTIONAL),
437 'attempt' => new external_value(PARAM_INT, 'Sequentially numbers this students attempts at this quiz.',
438 VALUE_OPTIONAL),
439 'uniqueid' => new external_value(PARAM_INT, 'Foreign key reference to the question_usage that holds the
440 details of the the question_attempts that make up this quiz
441 attempt.', VALUE_OPTIONAL),
442 'layout' => new external_value(PARAM_RAW, 'Attempt layout.', VALUE_OPTIONAL),
443 'currentpage' => new external_value(PARAM_INT, 'Attempt current page.', VALUE_OPTIONAL),
444 'preview' => new external_value(PARAM_INT, 'Whether is a preview attempt or not.', VALUE_OPTIONAL),
445 'state' => new external_value(PARAM_ALPHA, 'The current state of the attempts. \'inprogress\',
446 \'overdue\', \'finished\' or \'abandoned\'.', VALUE_OPTIONAL),
447 'timestart' => new external_value(PARAM_INT, 'Time when the attempt was started.', VALUE_OPTIONAL),
448 'timefinish' => new external_value(PARAM_INT, 'Time when the attempt was submitted.
449 0 if the attempt has not been submitted yet.', VALUE_OPTIONAL),
450 'timemodified' => new external_value(PARAM_INT, 'Last modified time.', VALUE_OPTIONAL),
8a972ab3 451 'timemodifiedoffline' => new external_value(PARAM_INT, 'Last modified time via webservices.', VALUE_OPTIONAL),
b8954440
JL
452 'timecheckstate' => new external_value(PARAM_INT, 'Next time quiz cron should check attempt for
453 state changes. NULL means never check.', VALUE_OPTIONAL),
454 'sumgrades' => new external_value(PARAM_FLOAT, 'Total marks for this attempt.', VALUE_OPTIONAL),
455 )
456 );
457 }
458
c161ecff
JL
459 /**
460 * Describes the get_user_attempts return value.
461 *
462 * @return external_single_structure
463 * @since Moodle 3.1
464 */
465 public static function get_user_attempts_returns() {
466 return new external_single_structure(
467 array(
b8954440 468 'attempts' => new external_multiple_structure(self::attempt_structure()),
c161ecff
JL
469 'warnings' => new external_warnings(),
470 )
471 );
472 }
473
e73e4581
JL
474 /**
475 * Describes the parameters for get_user_best_grade.
476 *
9db43c73 477 * @return external_function_parameters
e73e4581
JL
478 * @since Moodle 3.1
479 */
480 public static function get_user_best_grade_parameters() {
481 return new external_function_parameters (
482 array(
483 'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
484 'userid' => new external_value(PARAM_INT, 'user id', VALUE_DEFAULT, 0),
485 )
486 );
487 }
488
489 /**
490 * Get the best current grade for the given user on a quiz.
491 *
492 * @param int $quizid quiz instance id
493 * @param int $userid user id
494 * @return array of warnings and the grade information
495 * @since Moodle 3.1
496 */
497 public static function get_user_best_grade($quizid, $userid = 0) {
498 global $DB, $USER;
499
500 $warnings = array();
501
502 $params = array(
503 'quizid' => $quizid,
504 'userid' => $userid,
505 );
506 $params = self::validate_parameters(self::get_user_best_grade_parameters(), $params);
507
a79a6361 508 list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
e73e4581
JL
509
510 // Default value for userid.
511 if (empty($params['userid'])) {
512 $params['userid'] = $USER->id;
513 }
514
515 $user = core_user::get_user($params['userid'], '*', MUST_EXIST);
516 core_user::require_active_user($user);
517
518 // Extra checks so only users with permissions can view other users attempts.
519 if ($USER->id != $user->id) {
520 require_capability('mod/quiz:viewreports', $context);
521 }
522
523 $result = array();
524 $grade = quiz_get_best_grade($quiz, $user->id);
525
526 if ($grade === null) {
527 $result['hasgrade'] = false;
528 } else {
529 $result['hasgrade'] = true;
530 $result['grade'] = $grade;
531 }
532 $result['warnings'] = $warnings;
533 return $result;
534 }
535
536 /**
537 * Describes the get_user_best_grade return value.
538 *
539 * @return external_single_structure
540 * @since Moodle 3.1
541 */
542 public static function get_user_best_grade_returns() {
543 return new external_single_structure(
544 array(
545 'hasgrade' => new external_value(PARAM_BOOL, 'Whether the user has a grade on the given quiz.'),
546 'grade' => new external_value(PARAM_FLOAT, 'The grade (only if the user has a grade).', VALUE_OPTIONAL),
547 'warnings' => new external_warnings(),
548 )
549 );
550 }
551
1f67c0b8
JL
552 /**
553 * Describes the parameters for get_combined_review_options.
554 *
9db43c73 555 * @return external_function_parameters
1f67c0b8
JL
556 * @since Moodle 3.1
557 */
558 public static function get_combined_review_options_parameters() {
559 return new external_function_parameters (
560 array(
561 'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
562 'userid' => new external_value(PARAM_INT, 'user id (empty for current user)', VALUE_DEFAULT, 0),
563
564 )
565 );
566 }
567
568 /**
569 * Combines the review options from a number of different quiz attempts.
570 *
571 * @param int $quizid quiz instance id
572 * @param int $userid user id (empty for current user)
573 * @return array of warnings and the review options
574 * @since Moodle 3.1
575 */
576 public static function get_combined_review_options($quizid, $userid = 0) {
577 global $DB, $USER;
578
579 $warnings = array();
580
581 $params = array(
582 'quizid' => $quizid,
583 'userid' => $userid,
584 );
585 $params = self::validate_parameters(self::get_combined_review_options_parameters(), $params);
586
a79a6361 587 list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
1f67c0b8
JL
588
589 // Default value for userid.
590 if (empty($params['userid'])) {
591 $params['userid'] = $USER->id;
592 }
593
594 $user = core_user::get_user($params['userid'], '*', MUST_EXIST);
595 core_user::require_active_user($user);
596
597 // Extra checks so only users with permissions can view other users attempts.
598 if ($USER->id != $user->id) {
599 require_capability('mod/quiz:viewreports', $context);
600 }
601
602 $attempts = quiz_get_user_attempts($quiz->id, $user->id, 'all', true);
603
604 $result = array();
605 $result['someoptions'] = [];
606 $result['alloptions'] = [];
607
608 list($someoptions, $alloptions) = quiz_get_combined_reviewoptions($quiz, $attempts);
609
610 foreach (array('someoptions', 'alloptions') as $typeofoption) {
611 foreach ($$typeofoption as $key => $value) {
612 $result[$typeofoption][] = array(
613 "name" => $key,
614 "value" => (!empty($value)) ? $value : 0
615 );
616 }
617 }
618
619 $result['warnings'] = $warnings;
620 return $result;
621 }
622
623 /**
624 * Describes the get_combined_review_options return value.
625 *
626 * @return external_single_structure
627 * @since Moodle 3.1
628 */
629 public static function get_combined_review_options_returns() {
630 return new external_single_structure(
631 array(
632 'someoptions' => new external_multiple_structure(
633 new external_single_structure(
634 array(
635 'name' => new external_value(PARAM_ALPHANUMEXT, 'option name'),
636 'value' => new external_value(PARAM_INT, 'option value'),
637 )
638 )
639 ),
640 'alloptions' => new external_multiple_structure(
641 new external_single_structure(
642 array(
643 'name' => new external_value(PARAM_ALPHANUMEXT, 'option name'),
644 'value' => new external_value(PARAM_INT, 'option value'),
645 )
646 )
647 ),
648 'warnings' => new external_warnings(),
649 )
650 );
651 }
652
b8954440
JL
653 /**
654 * Describes the parameters for start_attempt.
655 *
9db43c73 656 * @return external_function_parameters
b8954440
JL
657 * @since Moodle 3.1
658 */
659 public static function start_attempt_parameters() {
660 return new external_function_parameters (
661 array(
662 'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
663 'preflightdata' => new external_multiple_structure(
664 new external_single_structure(
665 array(
666 'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
667 'value' => new external_value(PARAM_RAW, 'data value'),
668 )
669 ), 'Preflight required data (like passwords)', VALUE_DEFAULT, array()
670 ),
671 'forcenew' => new external_value(PARAM_BOOL, 'Whether to force a new attempt or not.', VALUE_DEFAULT, false),
672
673 )
674 );
675 }
676
677 /**
678 * Starts a new attempt at a quiz.
679 *
680 * @param int $quizid quiz instance id
681 * @param array $preflightdata preflight required data (like passwords)
682 * @param bool $forcenew Whether to force a new attempt or not.
683 * @return array of warnings and the attempt basic data
684 * @since Moodle 3.1
685 * @throws moodle_quiz_exception
686 */
687 public static function start_attempt($quizid, $preflightdata = array(), $forcenew = false) {
688 global $DB, $USER;
689
690 $warnings = array();
691 $attempt = array();
692
693 $params = array(
694 'quizid' => $quizid,
695 'preflightdata' => $preflightdata,
696 'forcenew' => $forcenew,
697 );
698 $params = self::validate_parameters(self::start_attempt_parameters(), $params);
699 $forcenew = $params['forcenew'];
700
a79a6361 701 list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
b8954440
JL
702
703 $quizobj = quiz::create($cm->instance, $USER->id);
704
705 // Check questions.
706 if (!$quizobj->has_questions()) {
707 throw new moodle_quiz_exception($quizobj, 'noquestionsfound');
708 }
709
710 // Create an object to manage all the other (non-roles) access rules.
711 $timenow = time();
712 $accessmanager = $quizobj->get_access_manager($timenow);
713
714 // Validate permissions for creating a new attempt and start a new preview attempt if required.
715 list($currentattemptid, $attemptnumber, $lastattempt, $messages, $page) =
716 quiz_validate_new_attempt($quizobj, $accessmanager, $forcenew, -1, false);
717
718 // Check access.
719 if (!$quizobj->is_preview_user() && $messages) {
720 // Create warnings with the exact messages.
721 foreach ($messages as $message) {
722 $warnings[] = array(
723 'item' => 'quiz',
724 'itemid' => $quiz->id,
725 'warningcode' => '1',
726 'message' => clean_text($message, PARAM_TEXT)
727 );
728 }
729 } else {
730 if ($accessmanager->is_preflight_check_required($currentattemptid)) {
731 // Need to do some checks before allowing the user to continue.
732
733 $provideddata = array();
734 foreach ($params['preflightdata'] as $data) {
735 $provideddata[$data['name']] = $data['value'];
736 }
737
738 $errors = $accessmanager->validate_preflight_check($provideddata, [], $currentattemptid);
739
740 if (!empty($errors)) {
741 throw new moodle_quiz_exception($quizobj, array_shift($errors));
742 }
743
744 // Pre-flight check passed.
745 $accessmanager->notify_preflight_check_passed($currentattemptid);
746 }
747
748 if ($currentattemptid) {
749 if ($lastattempt->state == quiz_attempt::OVERDUE) {
750 throw new moodle_quiz_exception($quizobj, 'stateoverdue');
751 } else {
752 throw new moodle_quiz_exception($quizobj, 'attemptstillinprogress');
753 }
754 }
8a972ab3
JL
755 $offlineattempt = WS_SERVER ? true : false;
756 $attempt = quiz_prepare_and_start_new_attempt($quizobj, $attemptnumber, $lastattempt, $offlineattempt);
b8954440
JL
757 }
758
759 $result = array();
760 $result['attempt'] = $attempt;
761 $result['warnings'] = $warnings;
762 return $result;
763 }
764
765 /**
766 * Describes the start_attempt return value.
767 *
768 * @return external_single_structure
769 * @since Moodle 3.1
770 */
771 public static function start_attempt_returns() {
772 return new external_single_structure(
773 array(
774 'attempt' => self::attempt_structure(),
775 'warnings' => new external_warnings(),
776 )
777 );
778 }
779
bc247b0d
JL
780 /**
781 * Utility function for validating a given attempt
782 *
98e68690
JL
783 * @param array $params array of parameters including the attemptid and preflight data
784 * @param bool $checkaccessrules whether to check the quiz access rules or not
785 * @param bool $failifoverdue whether to return error if the attempt is overdue
bc247b0d
JL
786 * @return array containing the attempt object and access messages
787 * @throws moodle_quiz_exception
788 * @since Moodle 3.1
789 */
98e68690 790 protected static function validate_attempt($params, $checkaccessrules = true, $failifoverdue = true) {
bc247b0d
JL
791 global $USER;
792
793 $attemptobj = quiz_attempt::create($params['attemptid']);
794
795 $context = context_module::instance($attemptobj->get_cm()->id);
796 self::validate_context($context);
797
798 // Check that this attempt belongs to this user.
799 if ($attemptobj->get_userid() != $USER->id) {
800 throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'notyourattempt');
801 }
802
803 // General capabilities check.
804 $ispreviewuser = $attemptobj->is_preview_user();
805 if (!$ispreviewuser) {
806 $attemptobj->require_capability('mod/quiz:attempt');
807 }
808
98e68690
JL
809 // Check the access rules.
810 $accessmanager = $attemptobj->get_access_manager(time());
811 $messages = array();
812 if ($checkaccessrules) {
813 // If the attempt is now overdue, or abandoned, deal with that.
814 $attemptobj->handle_if_time_expired(time(), true);
815
816 $messages = $accessmanager->prevent_access();
817 if (!$ispreviewuser && $messages) {
818 throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'attempterror');
819 }
820 }
821
bc247b0d
JL
822 // Attempt closed?.
823 if ($attemptobj->is_finished()) {
824 throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'attemptalreadyclosed');
98e68690 825 } else if ($failifoverdue && $attemptobj->get_state() == quiz_attempt::OVERDUE) {
bc247b0d
JL
826 throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'stateoverdue');
827 }
828
bc247b0d
JL
829 // User submitted data (like the quiz password).
830 if ($accessmanager->is_preflight_check_required($attemptobj->get_attemptid())) {
831 $provideddata = array();
832 foreach ($params['preflightdata'] as $data) {
833 $provideddata[$data['name']] = $data['value'];
834 }
835
836 $errors = $accessmanager->validate_preflight_check($provideddata, [], $params['attemptid']);
837 if (!empty($errors)) {
838 throw new moodle_quiz_exception($attemptobj->get_quizobj(), array_shift($errors));
839 }
840 // Pre-flight check passed.
841 $accessmanager->notify_preflight_check_passed($params['attemptid']);
842 }
843
bc9733e7
JL
844 if (isset($params['page'])) {
845 // Check if the page is out of range.
846 if ($params['page'] != $attemptobj->force_page_number_into_range($params['page'])) {
847 throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'Invalid page number');
848 }
849
850 // Prevent out of sequence access.
899983ee
JL
851 if (!$attemptobj->check_page_access($params['page'])) {
852 throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'Out of sequence access');
bc247b0d 853 }
bc247b0d 854
bc9733e7
JL
855 // Check slots.
856 $slots = $attemptobj->get_slots($params['page']);
bc247b0d 857
bc9733e7
JL
858 if (empty($slots)) {
859 throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'noquestionsfound');
860 }
bc247b0d
JL
861 }
862
863 return array($attemptobj, $messages);
864 }
865
866 /**
867 * Describes a single question structure.
868 *
01a79b4e 869 * @return external_single_structure the question data. Some fields may not be returned depending on the quiz display settings.
bc247b0d 870 * @since Moodle 3.1
33ef5cdc 871 * @since Moodle 3.2 blockedbyprevious parameter added.
bc247b0d
JL
872 */
873 private static function question_structure() {
874 return new external_single_structure(
875 array(
876 'slot' => new external_value(PARAM_INT, 'slot number'),
877 'type' => new external_value(PARAM_ALPHANUMEXT, 'question type, i.e: multichoice'),
878 'page' => new external_value(PARAM_INT, 'page of the quiz this question appears on'),
879 'html' => new external_value(PARAM_RAW, 'the question rendered'),
e49ccc92
JL
880 'responsefileareas' => new external_multiple_structure(
881 new external_single_structure(
882 array(
883 'area' => new external_value(PARAM_NOTAGS, 'File area name'),
884 'files' => new external_files('Response files for the question', VALUE_OPTIONAL),
885 )
886 ), 'Response file areas including files', VALUE_OPTIONAL
887 ),
74804b57
JL
888 'sequencecheck' => new external_value(PARAM_INT, 'the number of real steps in this attempt', VALUE_OPTIONAL),
889 'lastactiontime' => new external_value(PARAM_INT, 'the timestamp of the most recent step in this question attempt',
890 VALUE_OPTIONAL),
891 'hasautosavedstep' => new external_value(PARAM_BOOL, 'whether this question attempt has autosaved data',
892 VALUE_OPTIONAL),
bc247b0d
JL
893 'flagged' => new external_value(PARAM_BOOL, 'whether the question is flagged or not'),
894 'number' => new external_value(PARAM_INT, 'question ordering number in the quiz', VALUE_OPTIONAL),
01a79b4e
JL
895 'state' => new external_value(PARAM_ALPHA, 'the state where the question is in.
896 It will not be returned if the user cannot see it due to the quiz display correctness settings.',
897 VALUE_OPTIONAL),
bc247b0d 898 'status' => new external_value(PARAM_RAW, 'current formatted state of the question', VALUE_OPTIONAL),
33ef5cdc 899 'blockedbyprevious' => new external_value(PARAM_BOOL, 'whether the question is blocked by the previous question',
01a79b4e
JL
900 VALUE_OPTIONAL),
901 'mark' => new external_value(PARAM_RAW, 'the mark awarded.
902 It will be returned only if the user is allowed to see it.', VALUE_OPTIONAL),
903 'maxmark' => new external_value(PARAM_FLOAT, 'the maximum mark possible for this question attempt.
904 It will be returned only if the user is allowed to see it.', VALUE_OPTIONAL),
a5dcaee3 905 'settings' => new external_value(PARAM_RAW, 'Question settings (JSON encoded).', VALUE_OPTIONAL),
01a79b4e
JL
906 ),
907 'The question data. Some fields may not be returned depending on the quiz display settings.'
bc247b0d
JL
908 );
909 }
910
911 /**
912 * Return questions information for a given attempt.
913 *
914 * @param quiz_attempt $attemptobj the quiz attempt object
915 * @param bool $review whether if we are in review mode or not
916 * @param mixed $page string 'all' or integer page number
917 * @return array array of questions including data
918 */
919 private static function get_attempt_questions_data(quiz_attempt $attemptobj, $review, $page = 'all') {
920 global $PAGE;
921
922 $questions = array();
923 $contextid = $attemptobj->get_quizobj()->get_context()->id;
924 $displayoptions = $attemptobj->get_display_options($review);
925 $renderer = $PAGE->get_renderer('mod_quiz');
e49ccc92 926 $contextid = $attemptobj->get_quizobj()->get_context()->id;
bc247b0d
JL
927
928 foreach ($attemptobj->get_slots($page) as $slot) {
e49ccc92
JL
929 $qtype = $attemptobj->get_question_type_name($slot);
930 $qattempt = $attemptobj->get_question_attempt($slot);
a5dcaee3 931 $questiondef = $qattempt->get_question(true);
e49ccc92
JL
932
933 // Get response files (for questions like essay that allows attachments).
934 $responsefileareas = [];
935 foreach (question_bank::get_qtype($qtype)->response_file_areas() as $area) {
936 if ($files = $attemptobj->get_question_attempt($slot)->get_last_qt_files($area, $contextid)) {
937 $responsefileareas[$area]['area'] = $area;
938 $responsefileareas[$area]['files'] = [];
939
940 foreach ($files as $file) {
941 $responsefileareas[$area]['files'][] = array(
942 'filename' => $file->get_filename(),
943 'fileurl' => $qattempt->get_response_file_url($file),
944 'filesize' => $file->get_filesize(),
945 'filepath' => $file->get_filepath(),
946 'mimetype' => $file->get_mimetype(),
947 'timemodified' => $file->get_timemodified(),
948 );
949 }
950 }
951 }
bc247b0d 952
a5dcaee3
JL
953 // Check display settings for question.
954 $settings = $questiondef->get_question_definition_for_external_rendering($qattempt, $displayoptions);
955
bc247b0d
JL
956 $question = array(
957 'slot' => $slot,
e49ccc92 958 'type' => $qtype,
bc247b0d
JL
959 'page' => $attemptobj->get_question_page($slot),
960 'flagged' => $attemptobj->is_question_flagged($slot),
74804b57 961 'html' => $attemptobj->render_question($slot, $review, $renderer) . $PAGE->requires->get_end_code(),
e49ccc92
JL
962 'responsefileareas' => $responsefileareas,
963 'sequencecheck' => $qattempt->get_sequence_check_count(),
964 'lastactiontime' => $qattempt->get_last_step()->get_timecreated(),
a5dcaee3
JL
965 'hasautosavedstep' => $qattempt->has_autosaved_step(),
966 'settings' => !empty($settings) ? json_encode($settings) : null,
bc247b0d
JL
967 );
968
969 if ($attemptobj->is_real_question($slot)) {
970 $question['number'] = $attemptobj->get_question_number($slot);
e49ccc92 971 $showcorrectness = $displayoptions->correctness && $qattempt->has_marks();
01a79b4e
JL
972 if ($showcorrectness) {
973 $question['state'] = (string) $attemptobj->get_question_state($slot);
974 }
bc247b0d 975 $question['status'] = $attemptobj->get_question_status($slot, $displayoptions->correctness);
33ef5cdc 976 $question['blockedbyprevious'] = $attemptobj->is_blocked_by_previous_question($slot);
bc247b0d
JL
977 }
978 if ($displayoptions->marks >= question_display_options::MAX_ONLY) {
e49ccc92 979 $question['maxmark'] = $qattempt->get_max_mark();
bc247b0d
JL
980 }
981 if ($displayoptions->marks >= question_display_options::MARK_AND_MAX) {
982 $question['mark'] = $attemptobj->get_question_mark($slot);
983 }
984
985 $questions[] = $question;
986 }
987 return $questions;
988 }
989
990 /**
991 * Describes the parameters for get_attempt_data.
992 *
9db43c73 993 * @return external_function_parameters
bc247b0d
JL
994 * @since Moodle 3.1
995 */
996 public static function get_attempt_data_parameters() {
997 return new external_function_parameters (
998 array(
999 'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1000 'page' => new external_value(PARAM_INT, 'page number'),
1001 'preflightdata' => new external_multiple_structure(
1002 new external_single_structure(
1003 array(
1004 'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
1005 'value' => new external_value(PARAM_RAW, 'data value'),
1006 )
1007 ), 'Preflight required data (like passwords)', VALUE_DEFAULT, array()
1008 )
1009 )
1010 );
1011 }
1012
1013 /**
1014 * Returns information for the given attempt page for a quiz attempt in progress.
1015 *
1016 * @param int $attemptid attempt id
1017 * @param int $page page number
1018 * @param array $preflightdata preflight required data (like passwords)
1019 * @return array of warnings and the attempt data, next page, message and questions
1020 * @since Moodle 3.1
1021 * @throws moodle_quiz_exceptions
1022 */
1023 public static function get_attempt_data($attemptid, $page, $preflightdata = array()) {
1024
1025 $warnings = array();
1026
1027 $params = array(
1028 'attemptid' => $attemptid,
1029 'page' => $page,
1030 'preflightdata' => $preflightdata,
1031 );
1032 $params = self::validate_parameters(self::get_attempt_data_parameters(), $params);
1033
1034 list($attemptobj, $messages) = self::validate_attempt($params);
1035
1036 if ($attemptobj->is_last_page($params['page'])) {
1037 $nextpage = -1;
1038 } else {
1039 $nextpage = $params['page'] + 1;
1040 }
1041
1042 $result = array();
1043 $result['attempt'] = $attemptobj->get_attempt();
1044 $result['messages'] = $messages;
1045 $result['nextpage'] = $nextpage;
1046 $result['warnings'] = $warnings;
1047 $result['questions'] = self::get_attempt_questions_data($attemptobj, false, $params['page']);
1048
1049 return $result;
1050 }
1051
1052 /**
1053 * Describes the get_attempt_data return value.
1054 *
1055 * @return external_single_structure
1056 * @since Moodle 3.1
1057 */
1058 public static function get_attempt_data_returns() {
1059 return new external_single_structure(
1060 array(
1061 'attempt' => self::attempt_structure(),
1062 'messages' => new external_multiple_structure(
1063 new external_value(PARAM_TEXT, 'access message'),
1064 'access messages, will only be returned for users with mod/quiz:preview capability,
1065 for other users this method will throw an exception if there are messages'),
1066 'nextpage' => new external_value(PARAM_INT, 'next page number'),
1067 'questions' => new external_multiple_structure(self::question_structure()),
1068 'warnings' => new external_warnings(),
1069 )
1070 );
1071 }
1072
bc9733e7
JL
1073 /**
1074 * Describes the parameters for get_attempt_summary.
1075 *
9db43c73 1076 * @return external_function_parameters
bc9733e7
JL
1077 * @since Moodle 3.1
1078 */
1079 public static function get_attempt_summary_parameters() {
1080 return new external_function_parameters (
1081 array(
1082 'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1083 'preflightdata' => new external_multiple_structure(
1084 new external_single_structure(
1085 array(
1086 'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
1087 'value' => new external_value(PARAM_RAW, 'data value'),
1088 )
1089 ), 'Preflight required data (like passwords)', VALUE_DEFAULT, array()
1090 )
1091 )
1092 );
1093 }
1094
1095 /**
1096 * Returns a summary of a quiz attempt before it is submitted.
1097 *
1098 * @param int $attemptid attempt id
1099 * @param int $preflightdata preflight required data (like passwords)
1100 * @return array of warnings and the attempt summary data for each question
1101 * @since Moodle 3.1
1102 */
1103 public static function get_attempt_summary($attemptid, $preflightdata = array()) {
1104
1105 $warnings = array();
1106
1107 $params = array(
1108 'attemptid' => $attemptid,
1109 'preflightdata' => $preflightdata,
1110 );
1111 $params = self::validate_parameters(self::get_attempt_summary_parameters(), $params);
1112
98e68690 1113 list($attemptobj, $messages) = self::validate_attempt($params, true, false);
bc9733e7
JL
1114
1115 $result = array();
1116 $result['warnings'] = $warnings;
1117 $result['questions'] = self::get_attempt_questions_data($attemptobj, false, 'all');
1118
1119 return $result;
1120 }
1121
1122 /**
1123 * Describes the get_attempt_summary return value.
1124 *
1125 * @return external_single_structure
1126 * @since Moodle 3.1
1127 */
1128 public static function get_attempt_summary_returns() {
1129 return new external_single_structure(
1130 array(
1131 'questions' => new external_multiple_structure(self::question_structure()),
1132 'warnings' => new external_warnings(),
1133 )
1134 );
1135 }
1136
96d5607c
JL
1137 /**
1138 * Describes the parameters for save_attempt.
1139 *
9db43c73 1140 * @return external_function_parameters
96d5607c
JL
1141 * @since Moodle 3.1
1142 */
1143 public static function save_attempt_parameters() {
1144 return new external_function_parameters (
1145 array(
1146 'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1147 'data' => new external_multiple_structure(
1148 new external_single_structure(
1149 array(
1150 'name' => new external_value(PARAM_RAW, 'data name'),
1151 'value' => new external_value(PARAM_RAW, 'data value'),
1152 )
1153 ), 'the data to be saved'
1154 ),
1155 'preflightdata' => new external_multiple_structure(
1156 new external_single_structure(
1157 array(
1158 'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
1159 'value' => new external_value(PARAM_RAW, 'data value'),
1160 )
1161 ), 'Preflight required data (like passwords)', VALUE_DEFAULT, array()
1162 )
1163 )
1164 );
1165 }
1166
1167 /**
1168 * Processes save requests during the quiz. This function is intended for the quiz auto-save feature.
1169 *
1170 * @param int $attemptid attempt id
1171 * @param array $data the data to be saved
1172 * @param array $preflightdata preflight required data (like passwords)
1173 * @return array of warnings and execution result
1174 * @since Moodle 3.1
1175 */
1176 public static function save_attempt($attemptid, $data, $preflightdata = array()) {
61772eb8 1177 global $DB, $USER;
96d5607c
JL
1178
1179 $warnings = array();
1180
1181 $params = array(
1182 'attemptid' => $attemptid,
1183 'data' => $data,
1184 'preflightdata' => $preflightdata,
1185 );
1186 $params = self::validate_parameters(self::save_attempt_parameters(), $params);
1187
1188 // Add a page, required by validate_attempt.
1189 list($attemptobj, $messages) = self::validate_attempt($params);
1190
61772eb8
JL
1191 // Prevent functions like file_get_submitted_draft_itemid() or form library requiring a sesskey for WS requests.
1192 if (WS_SERVER || PHPUNIT_TEST) {
1193 $USER->ignoresesskey = true;
1194 }
96d5607c
JL
1195 $transaction = $DB->start_delegated_transaction();
1196 // Create the $_POST object required by the question engine.
1197 $_POST = array();
1198 foreach ($data as $element) {
1199 $_POST[$element['name']] = $element['value'];
61772eb8
JL
1200 // Some deep core functions like file_get_submitted_draft_itemid() also requires $_REQUEST to be filled.
1201 $_REQUEST[$element['name']] = $element['value'];
96d5607c
JL
1202 }
1203 $timenow = time();
8a972ab3
JL
1204 // Update the timemodifiedoffline field.
1205 $attemptobj->set_offline_modified_time($timenow);
96d5607c
JL
1206 $attemptobj->process_auto_save($timenow);
1207 $transaction->allow_commit();
1208
1209 $result = array();
1210 $result['status'] = true;
1211 $result['warnings'] = $warnings;
1212 return $result;
1213 }
1214
1215 /**
1216 * Describes the save_attempt return value.
1217 *
1218 * @return external_single_structure
1219 * @since Moodle 3.1
1220 */
1221 public static function save_attempt_returns() {
1222 return new external_single_structure(
1223 array(
1224 'status' => new external_value(PARAM_BOOL, 'status: true if success'),
1225 'warnings' => new external_warnings(),
1226 )
1227 );
1228 }
1229
98e68690
JL
1230 /**
1231 * Describes the parameters for process_attempt.
1232 *
9db43c73 1233 * @return external_function_parameters
98e68690
JL
1234 * @since Moodle 3.1
1235 */
1236 public static function process_attempt_parameters() {
1237 return new external_function_parameters (
1238 array(
1239 'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1240 'data' => new external_multiple_structure(
1241 new external_single_structure(
1242 array(
1243 'name' => new external_value(PARAM_RAW, 'data name'),
1244 'value' => new external_value(PARAM_RAW, 'data value'),
1245 )
1246 ),
1247 'the data to be saved', VALUE_DEFAULT, array()
1248 ),
1249 'finishattempt' => new external_value(PARAM_BOOL, 'whether to finish or not the attempt', VALUE_DEFAULT, false),
1250 'timeup' => new external_value(PARAM_BOOL, 'whether the WS was called by a timer when the time is up',
1251 VALUE_DEFAULT, false),
1252 'preflightdata' => new external_multiple_structure(
1253 new external_single_structure(
1254 array(
1255 'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
1256 'value' => new external_value(PARAM_RAW, 'data value'),
1257 )
1258 ), 'Preflight required data (like passwords)', VALUE_DEFAULT, array()
1259 )
1260 )
1261 );
1262 }
1263
1264 /**
1265 * Process responses during an attempt at a quiz and also deals with attempts finishing.
1266 *
1267 * @param int $attemptid attempt id
1268 * @param array $data the data to be saved
1269 * @param bool $finishattempt whether to finish or not the attempt
1270 * @param bool $timeup whether the WS was called by a timer when the time is up
1271 * @param array $preflightdata preflight required data (like passwords)
1272 * @return array of warnings and the attempt state after the processing
1273 * @since Moodle 3.1
1274 */
1275 public static function process_attempt($attemptid, $data, $finishattempt = false, $timeup = false, $preflightdata = array()) {
61772eb8 1276 global $USER;
98e68690
JL
1277
1278 $warnings = array();
1279
1280 $params = array(
1281 'attemptid' => $attemptid,
1282 'data' => $data,
1283 'finishattempt' => $finishattempt,
1284 'timeup' => $timeup,
1285 'preflightdata' => $preflightdata,
1286 );
1287 $params = self::validate_parameters(self::process_attempt_parameters(), $params);
1288
904c37c6
JL
1289 // Do not check access manager rules and evaluate fail if overdue.
1290 $attemptobj = quiz_attempt::create($params['attemptid']);
1291 $failifoverdue = !($attemptobj->get_quizobj()->get_quiz()->overduehandling == 'graceperiod');
1292
1293 list($attemptobj, $messages) = self::validate_attempt($params, false, $failifoverdue);
98e68690 1294
61772eb8
JL
1295 // Prevent functions like file_get_submitted_draft_itemid() or form library requiring a sesskey for WS requests.
1296 if (WS_SERVER || PHPUNIT_TEST) {
1297 $USER->ignoresesskey = true;
1298 }
98e68690
JL
1299 // Create the $_POST object required by the question engine.
1300 $_POST = array();
1301 foreach ($params['data'] as $element) {
1302 $_POST[$element['name']] = $element['value'];
61772eb8 1303 $_REQUEST[$element['name']] = $element['value'];
98e68690
JL
1304 }
1305 $timenow = time();
1306 $finishattempt = $params['finishattempt'];
1307 $timeup = $params['timeup'];
1308
1309 $result = array();
8a972ab3
JL
1310 // Update the timemodifiedoffline field.
1311 $attemptobj->set_offline_modified_time($timenow);
98e68690 1312 $result['state'] = $attemptobj->process_attempt($timenow, $finishattempt, $timeup, 0);
8a972ab3 1313
98e68690
JL
1314 $result['warnings'] = $warnings;
1315 return $result;
1316 }
1317
1318 /**
1319 * Describes the process_attempt return value.
1320 *
1321 * @return external_single_structure
1322 * @since Moodle 3.1
1323 */
1324 public static function process_attempt_returns() {
1325 return new external_single_structure(
1326 array(
1327 'state' => new external_value(PARAM_ALPHANUMEXT, 'state: the new attempt state:
1328 inprogress, finished, overdue, abandoned'),
1329 'warnings' => new external_warnings(),
1330 )
1331 );
1332 }
1333
3589b659
JL
1334 /**
1335 * Validate an attempt finished for review. The attempt would be reviewed by a user or a teacher.
1336 *
1337 * @param array $params Array of parameters including the attemptid
1338 * @return array containing the attempt object and display options
1339 * @since Moodle 3.1
1340 * @throws moodle_exception
1341 * @throws moodle_quiz_exception
1342 */
1343 protected static function validate_attempt_review($params) {
1344
1345 $attemptobj = quiz_attempt::create($params['attemptid']);
1346 $attemptobj->check_review_capability();
1347
1348 $displayoptions = $attemptobj->get_display_options(true);
1349 if ($attemptobj->is_own_attempt()) {
1350 if (!$attemptobj->is_finished()) {
1351 throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'attemptclosed');
1352 } else if (!$displayoptions->attempt) {
ee766461
JL
1353 throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'noreview', null, '',
1354 $attemptobj->cannot_review_message());
3589b659
JL
1355 }
1356 } else if (!$attemptobj->is_review_allowed()) {
1357 throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'noreviewattempt');
1358 }
1359 return array($attemptobj, $displayoptions);
1360 }
1361
1362 /**
1363 * Describes the parameters for get_attempt_review.
1364 *
9db43c73 1365 * @return external_function_parameters
3589b659
JL
1366 * @since Moodle 3.1
1367 */
1368 public static function get_attempt_review_parameters() {
1369 return new external_function_parameters (
1370 array(
1371 'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1372 'page' => new external_value(PARAM_INT, 'page number, empty for all the questions in all the pages',
1373 VALUE_DEFAULT, -1),
1374 )
1375 );
1376 }
1377
1378 /**
1379 * Returns review information for the given finished attempt, can be used by users or teachers.
1380 *
1381 * @param int $attemptid attempt id
1382 * @param int $page page number, empty for all the questions in all the pages
1383 * @return array of warnings and the attempt data, feedback and questions
1384 * @since Moodle 3.1
1385 * @throws moodle_exception
1386 * @throws moodle_quiz_exception
1387 */
1388 public static function get_attempt_review($attemptid, $page = -1) {
1389 global $PAGE;
1390
1391 $warnings = array();
1392
1393 $params = array(
1394 'attemptid' => $attemptid,
1395 'page' => $page,
1396 );
1397 $params = self::validate_parameters(self::get_attempt_review_parameters(), $params);
1398
1399 list($attemptobj, $displayoptions) = self::validate_attempt_review($params);
1400
1401 if ($params['page'] !== -1) {
1402 $page = $attemptobj->force_page_number_into_range($params['page']);
1403 } else {
1404 $page = 'all';
1405 }
1406
1407 // Prepare the output.
1408 $result = array();
1409 $result['attempt'] = $attemptobj->get_attempt();
1410 $result['questions'] = self::get_attempt_questions_data($attemptobj, true, $page, true);
1411
1412 $result['additionaldata'] = array();
1413 // Summary data (from behaviours).
1414 $summarydata = $attemptobj->get_additional_summary_data($displayoptions);
1415 foreach ($summarydata as $key => $data) {
1416 // This text does not need formatting (no need for external_format_[string|text]).
1417 $result['additionaldata'][] = array(
1418 'id' => $key,
1419 'title' => $data['title'], $attemptobj->get_quizobj()->get_context()->id,
1420 'content' => $data['content'],
1421 );
1422 }
1423
1424 // Feedback if there is any, and the user is allowed to see it now.
1425 $grade = quiz_rescale_grade($attemptobj->get_attempt()->sumgrades, $attemptobj->get_quiz(), false);
1426
1427 $feedback = $attemptobj->get_overall_feedback($grade);
1428 if ($displayoptions->overallfeedback && $feedback) {
1429 $result['additionaldata'][] = array(
1430 'id' => 'feedback',
1431 'title' => get_string('feedback', 'quiz'),
1432 'content' => $feedback,
1433 );
1434 }
1435
1436 $result['grade'] = $grade;
1437 $result['warnings'] = $warnings;
1438 return $result;
1439 }
1440
1441 /**
1442 * Describes the get_attempt_review return value.
1443 *
1444 * @return external_single_structure
1445 * @since Moodle 3.1
1446 */
1447 public static function get_attempt_review_returns() {
1448 return new external_single_structure(
1449 array(
1450 'grade' => new external_value(PARAM_RAW, 'grade for the quiz (or empty or "notyetgraded")'),
1451 'attempt' => self::attempt_structure(),
1452 'additionaldata' => new external_multiple_structure(
1453 new external_single_structure(
1454 array(
1455 'id' => new external_value(PARAM_ALPHANUMEXT, 'id of the data'),
1456 'title' => new external_value(PARAM_TEXT, 'data title'),
1457 'content' => new external_value(PARAM_RAW, 'data content'),
1458 )
1459 )
1460 ),
1461 'questions' => new external_multiple_structure(self::question_structure()),
1462 'warnings' => new external_warnings(),
1463 )
1464 );
1465 }
1466
899983ee
JL
1467 /**
1468 * Describes the parameters for view_attempt.
1469 *
9db43c73 1470 * @return external_function_parameters
899983ee
JL
1471 * @since Moodle 3.1
1472 */
1473 public static function view_attempt_parameters() {
1474 return new external_function_parameters (
1475 array(
1476 'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1477 'page' => new external_value(PARAM_INT, 'page number'),
9be3072d
JL
1478 'preflightdata' => new external_multiple_structure(
1479 new external_single_structure(
1480 array(
1481 'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
1482 'value' => new external_value(PARAM_RAW, 'data value'),
1483 )
1484 ), 'Preflight required data (like passwords)', VALUE_DEFAULT, array()
1485 )
899983ee
JL
1486 )
1487 );
1488 }
1489
1490 /**
1491 * Trigger the attempt viewed event.
1492 *
1493 * @param int $attemptid attempt id
1494 * @param int $page page number
9be3072d 1495 * @param array $preflightdata preflight required data (like passwords)
899983ee
JL
1496 * @return array of warnings and status result
1497 * @since Moodle 3.1
1498 */
9be3072d 1499 public static function view_attempt($attemptid, $page, $preflightdata = array()) {
899983ee
JL
1500
1501 $warnings = array();
1502
1503 $params = array(
1504 'attemptid' => $attemptid,
1505 'page' => $page,
9be3072d 1506 'preflightdata' => $preflightdata,
899983ee
JL
1507 );
1508 $params = self::validate_parameters(self::view_attempt_parameters(), $params);
1509 list($attemptobj, $messages) = self::validate_attempt($params);
1510
1511 // Log action.
1512 $attemptobj->fire_attempt_viewed_event();
1513
1514 // Update attempt page, throwing an exception if $page is not valid.
1515 if (!$attemptobj->set_currentpage($params['page'])) {
1516 throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'Out of sequence access');
1517 }
1518
1519 $result = array();
1520 $result['status'] = true;
1521 $result['warnings'] = $warnings;
1522 return $result;
1523 }
1524
1525 /**
1526 * Describes the view_attempt return value.
1527 *
1528 * @return external_single_structure
1529 * @since Moodle 3.1
1530 */
1531 public static function view_attempt_returns() {
1532 return new external_single_structure(
1533 array(
1534 'status' => new external_value(PARAM_BOOL, 'status: true if success'),
1535 'warnings' => new external_warnings(),
1536 )
1537 );
1538 }
1539
d9ef6ae0
JL
1540 /**
1541 * Describes the parameters for view_attempt_summary.
1542 *
9db43c73 1543 * @return external_function_parameters
d9ef6ae0
JL
1544 * @since Moodle 3.1
1545 */
1546 public static function view_attempt_summary_parameters() {
1547 return new external_function_parameters (
1548 array(
1549 'attemptid' => new external_value(PARAM_INT, 'attempt id'),
9be3072d
JL
1550 'preflightdata' => new external_multiple_structure(
1551 new external_single_structure(
1552 array(
1553 'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
1554 'value' => new external_value(PARAM_RAW, 'data value'),
1555 )
1556 ), 'Preflight required data (like passwords)', VALUE_DEFAULT, array()
1557 )
d9ef6ae0
JL
1558 )
1559 );
1560 }
1561
1562 /**
1563 * Trigger the attempt summary viewed event.
1564 *
1565 * @param int $attemptid attempt id
9be3072d 1566 * @param array $preflightdata preflight required data (like passwords)
d9ef6ae0
JL
1567 * @return array of warnings and status result
1568 * @since Moodle 3.1
1569 */
9be3072d 1570 public static function view_attempt_summary($attemptid, $preflightdata = array()) {
d9ef6ae0
JL
1571
1572 $warnings = array();
1573
1574 $params = array(
1575 'attemptid' => $attemptid,
9be3072d 1576 'preflightdata' => $preflightdata,
d9ef6ae0
JL
1577 );
1578 $params = self::validate_parameters(self::view_attempt_summary_parameters(), $params);
1579 list($attemptobj, $messages) = self::validate_attempt($params);
1580
1581 // Log action.
1582 $attemptobj->fire_attempt_summary_viewed_event();
1583
1584 $result = array();
1585 $result['status'] = true;
1586 $result['warnings'] = $warnings;
1587 return $result;
1588 }
1589
1590 /**
1591 * Describes the view_attempt_summary return value.
1592 *
1593 * @return external_single_structure
1594 * @since Moodle 3.1
1595 */
1596 public static function view_attempt_summary_returns() {
1597 return new external_single_structure(
1598 array(
1599 'status' => new external_value(PARAM_BOOL, 'status: true if success'),
1600 'warnings' => new external_warnings(),
1601 )
1602 );
1603 }
1604
3e5c19a0
JL
1605 /**
1606 * Describes the parameters for view_attempt_review.
1607 *
9db43c73 1608 * @return external_function_parameters
3e5c19a0
JL
1609 * @since Moodle 3.1
1610 */
1611 public static function view_attempt_review_parameters() {
1612 return new external_function_parameters (
1613 array(
1614 'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1615 )
1616 );
1617 }
1618
1619 /**
1620 * Trigger the attempt reviewed event.
1621 *
1622 * @param int $attemptid attempt id
1623 * @return array of warnings and status result
1624 * @since Moodle 3.1
1625 */
1626 public static function view_attempt_review($attemptid) {
1627
1628 $warnings = array();
1629
1630 $params = array(
1631 'attemptid' => $attemptid,
1632 );
1633 $params = self::validate_parameters(self::view_attempt_review_parameters(), $params);
1634 list($attemptobj, $displayoptions) = self::validate_attempt_review($params);
1635
1636 // Log action.
1637 $attemptobj->fire_attempt_reviewed_event();
1638
1639 $result = array();
1640 $result['status'] = true;
1641 $result['warnings'] = $warnings;
1642 return $result;
1643 }
1644
1645 /**
1646 * Describes the view_attempt_review return value.
1647 *
1648 * @return external_single_structure
1649 * @since Moodle 3.1
1650 */
1651 public static function view_attempt_review_returns() {
1652 return new external_single_structure(
1653 array(
1654 'status' => new external_value(PARAM_BOOL, 'status: true if success'),
1655 'warnings' => new external_warnings(),
1656 )
1657 );
1658 }
1659
48abca79
JL
1660 /**
1661 * Describes the parameters for view_quiz.
1662 *
9db43c73 1663 * @return external_function_parameters
48abca79
JL
1664 * @since Moodle 3.1
1665 */
1666 public static function get_quiz_feedback_for_grade_parameters() {
1667 return new external_function_parameters (
1668 array(
1669 'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
1670 'grade' => new external_value(PARAM_FLOAT, 'the grade to check'),
1671 )
1672 );
1673 }
1674
1675 /**
1676 * Get the feedback text that should be show to a student who got the given grade in the given quiz.
1677 *
1678 * @param int $quizid quiz instance id
1679 * @param float $grade the grade to check
1680 * @return array of warnings and status result
1681 * @since Moodle 3.1
1682 * @throws moodle_exception
1683 */
1684 public static function get_quiz_feedback_for_grade($quizid, $grade) {
1685 global $DB;
1686
1687 $params = array(
1688 'quizid' => $quizid,
1689 'grade' => $grade,
1690 );
1691 $params = self::validate_parameters(self::get_quiz_feedback_for_grade_parameters(), $params);
1692 $warnings = array();
1693
a79a6361 1694 list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
48abca79
JL
1695
1696 $result = array();
1697 $result['feedbacktext'] = '';
1698 $result['feedbacktextformat'] = FORMAT_MOODLE;
1699
1700 $feedback = quiz_feedback_record_for_grade($params['grade'], $quiz);
1701 if (!empty($feedback->feedbacktext)) {
1702 list($text, $format) = external_format_text($feedback->feedbacktext, $feedback->feedbacktextformat, $context->id,
1703 'mod_quiz', 'feedback', $feedback->id);
1704 $result['feedbacktext'] = $text;
1705 $result['feedbacktextformat'] = $format;
c8743f62
JL
1706 $feedbackinlinefiles = external_util::get_area_files($context->id, 'mod_quiz', 'feedback', $feedback->id);
1707 if (!empty($feedbackinlinefiles)) {
1708 $result['feedbackinlinefiles'] = $feedbackinlinefiles;
1709 }
48abca79
JL
1710 }
1711
1712 $result['warnings'] = $warnings;
1713 return $result;
1714 }
1715
1716 /**
1717 * Describes the get_quiz_feedback_for_grade return value.
1718 *
1719 * @return external_single_structure
1720 * @since Moodle 3.1
1721 */
1722 public static function get_quiz_feedback_for_grade_returns() {
1723 return new external_single_structure(
1724 array(
1725 'feedbacktext' => new external_value(PARAM_RAW, 'the comment that corresponds to this grade (empty for none)'),
1726 'feedbacktextformat' => new external_format_value('feedbacktext', VALUE_OPTIONAL),
c8743f62 1727 'feedbackinlinefiles' => new external_files('feedback inline files', VALUE_OPTIONAL),
48abca79
JL
1728 'warnings' => new external_warnings(),
1729 )
1730 );
1731 }
1732
a79a6361
JL
1733 /**
1734 * Describes the parameters for get_quiz_access_information.
1735 *
9db43c73 1736 * @return external_function_parameters
a79a6361
JL
1737 * @since Moodle 3.1
1738 */
1739 public static function get_quiz_access_information_parameters() {
1740 return new external_function_parameters (
1741 array(
1742 'quizid' => new external_value(PARAM_INT, 'quiz instance id')
1743 )
1744 );
1745 }
1746
1747 /**
1748 * Return access information for a given quiz.
1749 *
1750 * @param int $quizid quiz instance id
1751 * @return array of warnings and the access information
1752 * @since Moodle 3.1
1753 * @throws moodle_quiz_exception
1754 */
1755 public static function get_quiz_access_information($quizid) {
1756 global $DB, $USER;
1757
1758 $warnings = array();
1759
1760 $params = array(
1761 'quizid' => $quizid
1762 );
1763 $params = self::validate_parameters(self::get_quiz_access_information_parameters(), $params);
1764
1765 list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
1766
1767 $result = array();
1768 // Capabilities first.
1769 $result['canattempt'] = has_capability('mod/quiz:attempt', $context);;
1770 $result['canmanage'] = has_capability('mod/quiz:manage', $context);;
1771 $result['canpreview'] = has_capability('mod/quiz:preview', $context);;
1772 $result['canreviewmyattempts'] = has_capability('mod/quiz:reviewmyattempts', $context);;
1773 $result['canviewreports'] = has_capability('mod/quiz:viewreports', $context);;
1774
1775 // Access manager now.
1776 $quizobj = quiz::create($cm->instance, $USER->id);
1777 $ignoretimelimits = has_capability('mod/quiz:ignoretimelimits', $context, null, false);
1778 $timenow = time();
1779 $accessmanager = new quiz_access_manager($quizobj, $timenow, $ignoretimelimits);
1780
1781 $result['accessrules'] = $accessmanager->describe_rules();
1782 $result['activerulenames'] = $accessmanager->get_active_rule_names();
1783 $result['preventaccessreasons'] = $accessmanager->prevent_access();
1784
1785 $result['warnings'] = $warnings;
1786 return $result;
1787 }
1788
1789 /**
1790 * Describes the get_quiz_access_information return value.
1791 *
1792 * @return external_single_structure
1793 * @since Moodle 3.1
1794 */
1795 public static function get_quiz_access_information_returns() {
1796 return new external_single_structure(
1797 array(
1798 'canattempt' => new external_value(PARAM_BOOL, 'Whether the user can do the quiz or not.'),
1799 'canmanage' => new external_value(PARAM_BOOL, 'Whether the user can edit the quiz settings or not.'),
1800 'canpreview' => new external_value(PARAM_BOOL, 'Whether the user can preview the quiz or not.'),
1801 'canreviewmyattempts' => new external_value(PARAM_BOOL, 'Whether the users can review their previous attempts
1802 or not.'),
1803 'canviewreports' => new external_value(PARAM_BOOL, 'Whether the user can view the quiz reports or not.'),
1804 'accessrules' => new external_multiple_structure(
1805 new external_value(PARAM_TEXT, 'rule description'), 'list of rules'),
1806 'activerulenames' => new external_multiple_structure(
1807 new external_value(PARAM_PLUGIN, 'rule plugin names'), 'list of active rules'),
1808 'preventaccessreasons' => new external_multiple_structure(
1809 new external_value(PARAM_TEXT, 'access restriction description'), 'list of reasons'),
1810 'warnings' => new external_warnings(),
1811 )
1812 );
1813 }
1814
ff99efcd
JL
1815 /**
1816 * Describes the parameters for get_attempt_access_information.
1817 *
9db43c73 1818 * @return external_function_parameters
ff99efcd
JL
1819 * @since Moodle 3.1
1820 */
1821 public static function get_attempt_access_information_parameters() {
1822 return new external_function_parameters (
1823 array(
1824 'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
1825 'attemptid' => new external_value(PARAM_INT, 'attempt id, 0 for the user last attempt if exists', VALUE_DEFAULT, 0),
1826 )
1827 );
1828 }
1829
1830 /**
1831 * Return access information for a given attempt in a quiz.
1832 *
1833 * @param int $quizid quiz instance id
1834 * @param int $attemptid attempt id, 0 for the user last attempt if exists
1835 * @return array of warnings and the access information
1836 * @since Moodle 3.1
1837 * @throws moodle_quiz_exception
1838 */
1839 public static function get_attempt_access_information($quizid, $attemptid = 0) {
1840 global $DB, $USER;
1841
1842 $warnings = array();
1843
1844 $params = array(
1845 'quizid' => $quizid,
1846 'attemptid' => $attemptid,
1847 );
1848 $params = self::validate_parameters(self::get_attempt_access_information_parameters(), $params);
1849
1850 list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
1851
1852 $attempttocheck = 0;
1853 if (!empty($params['attemptid'])) {
1854 $attemptobj = quiz_attempt::create($params['attemptid']);
1855 if ($attemptobj->get_userid() != $USER->id) {
1856 throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'notyourattempt');
1857 }
1858 $attempttocheck = $attemptobj->get_attempt();
1859 }
1860
1861 // Access manager now.
1862 $quizobj = quiz::create($cm->instance, $USER->id);
1863 $ignoretimelimits = has_capability('mod/quiz:ignoretimelimits', $context, null, false);
1864 $timenow = time();
1865 $accessmanager = new quiz_access_manager($quizobj, $timenow, $ignoretimelimits);
1866
1867 $attempts = quiz_get_user_attempts($quiz->id, $USER->id, 'finished', true);
1868 $lastfinishedattempt = end($attempts);
1869 if ($unfinishedattempt = quiz_get_user_attempt_unfinished($quiz->id, $USER->id)) {
1870 $attempts[] = $unfinishedattempt;
1871
1872 // Check if the attempt is now overdue. In that case the state will change.
1873 $quizobj->create_attempt_object($unfinishedattempt)->handle_if_time_expired(time(), false);
1874
1875 if ($unfinishedattempt->state != quiz_attempt::IN_PROGRESS and $unfinishedattempt->state != quiz_attempt::OVERDUE) {
1876 $lastfinishedattempt = $unfinishedattempt;
1877 }
1878 }
1879 $numattempts = count($attempts);
1880
1881 if (!$attempttocheck) {
1882 $attempttocheck = $unfinishedattempt ? $unfinishedattempt : $lastfinishedattempt;
1883 }
1884
1885 $result = array();
1886 $result['isfinished'] = $accessmanager->is_finished($numattempts, $lastfinishedattempt);
1887 $result['preventnewattemptreasons'] = $accessmanager->prevent_new_attempt($numattempts, $lastfinishedattempt);
1888
1889 if ($attempttocheck) {
1890 $endtime = $accessmanager->get_end_time($attempttocheck);
1891 $result['endtime'] = ($endtime === false) ? 0 : $endtime;
1892 $attemptid = $unfinishedattempt ? $unfinishedattempt->id : null;
1893 $result['ispreflightcheckrequired'] = $accessmanager->is_preflight_check_required($attemptid);
1894 }
1895
1896 $result['warnings'] = $warnings;
1897 return $result;
1898 }
1899
1900 /**
1901 * Describes the get_attempt_access_information return value.
1902 *
1903 * @return external_single_structure
1904 * @since Moodle 3.1
1905 */
1906 public static function get_attempt_access_information_returns() {
1907 return new external_single_structure(
1908 array(
1909 'endtime' => new external_value(PARAM_INT, 'When the attempt must be submitted (determined by rules).',
1910 VALUE_OPTIONAL),
1911 'isfinished' => new external_value(PARAM_BOOL, 'Whether there is no way the user will ever be allowed to attempt.'),
1912 'ispreflightcheckrequired' => new external_value(PARAM_BOOL, 'whether a check is required before the user
1913 starts/continues his attempt.', VALUE_OPTIONAL),
1914 'preventnewattemptreasons' => new external_multiple_structure(
1915 new external_value(PARAM_TEXT, 'access restriction description'),
1916 'list of reasons'),
1917 'warnings' => new external_warnings(),
1918 )
1919 );
1920 }
1921
efca239e
JL
1922 /**
1923 * Describes the parameters for get_quiz_required_qtypes.
1924 *
9db43c73 1925 * @return external_function_parameters
efca239e
JL
1926 * @since Moodle 3.1
1927 */
1928 public static function get_quiz_required_qtypes_parameters() {
1929 return new external_function_parameters (
1930 array(
1931 'quizid' => new external_value(PARAM_INT, 'quiz instance id')
1932 )
1933 );
1934 }
1935
1936 /**
1937 * Return the potential question types that would be required for a given quiz.
1938 * Please note that for random question types we return the potential question types in the category choosen.
1939 *
1940 * @param int $quizid quiz instance id
1941 * @return array of warnings and the access information
1942 * @since Moodle 3.1
1943 * @throws moodle_quiz_exception
1944 */
1945 public static function get_quiz_required_qtypes($quizid) {
1946 global $DB, $USER;
1947
1948 $warnings = array();
1949
1950 $params = array(
1951 'quizid' => $quizid
1952 );
1953 $params = self::validate_parameters(self::get_quiz_required_qtypes_parameters(), $params);
1954
1955 list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
1956
1957 $quizobj = quiz::create($cm->instance, $USER->id);
1958 $quizobj->preload_questions();
1959 $quizobj->load_questions();
1960
1961 // Question types used.
1962 $result = array();
1963 $result['questiontypes'] = $quizobj->get_all_question_types_used(true);
1964 $result['warnings'] = $warnings;
1965 return $result;
1966 }
1967
1968 /**
1969 * Describes the get_quiz_required_qtypes return value.
1970 *
1971 * @return external_single_structure
1972 * @since Moodle 3.1
1973 */
1974 public static function get_quiz_required_qtypes_returns() {
1975 return new external_single_structure(
1976 array(
1977 'questiontypes' => new external_multiple_structure(
1978 new external_value(PARAM_PLUGIN, 'question type'), 'list of question types used in the quiz'),
1979 'warnings' => new external_warnings(),
1980 )
1981 );
1982 }
1983
51e27aac 1984}