Proyectos de Subversion Moodle

Rev

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