Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

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