Proyectos de Subversion Moodle

Rev

Rev 11 | | 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 module external functions tests.
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
namespace mod_quiz\external;
28
 
29
use core_external\external_api;
30
use core_question\local\bank\question_version_status;
31
use externallib_advanced_testcase;
32
use mod_quiz\question\display_options;
33
use mod_quiz\quiz_attempt;
34
use mod_quiz\quiz_settings;
35
use mod_quiz\structure;
36
use mod_quiz_external;
37
use moodle_exception;
38
 
39
defined('MOODLE_INTERNAL') || die();
40
 
41
global $CFG;
42
 
43
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
44
require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
45
 
46
/**
47
 * Silly class to access mod_quiz_external internal methods.
48
 *
49
 * @package mod_quiz
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 testable_mod_quiz_external extends mod_quiz_external {
55
 
56
    /**
57
     * Public accessor.
58
     *
59
     * @param  array $params Array of parameters including the attemptid and preflight data
60
     * @param  bool $checkaccessrules whether to check the quiz access rules or not
61
     * @param  bool $failifoverdue whether to return error if the attempt is overdue
62
     * @return  array containing the attempt object and access messages
63
     */
64
    public static function validate_attempt($params, $checkaccessrules = true, $failifoverdue = true) {
65
        return parent::validate_attempt($params, $checkaccessrules, $failifoverdue);
66
    }
67
 
68
    /**
69
     * Public accessor.
70
     *
71
     * @param  array $params Array of parameters including the attemptid
72
     * @return  array containing the attempt object and display options
73
     */
74
    public static function validate_attempt_review($params) {
75
        return parent::validate_attempt_review($params);
76
    }
77
}
78
 
79
/**
80
 * Quiz module external functions tests
81
 *
82
 * @package    mod_quiz
83
 * @category   external
84
 * @copyright  2016 Juan Leyva <juan@moodle.com>
85
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
86
 * @since      Moodle 3.1
87
 * @covers \mod_quiz_external
88
 */
1441 ariadna 89
final class external_test extends externallib_advanced_testcase {
1 efrain 90
 
91
    use \quiz_question_helper_test_trait;
92
 
93
    /** @var \stdClass course record. */
94
    protected $course;
95
 
96
    /** @var \stdClass activity record. */
97
    protected $quiz;
98
 
99
    /** @var \context_module context instance. */
100
    protected $context;
101
 
102
    /** @var \stdClass */
103
    protected $cm;
104
 
105
    /** @var \stdClass user record. */
106
    protected $student;
107
 
108
    /** @var \stdClass user record. */
109
    protected $teacher;
110
 
111
    /** @var \stdClass user role record. */
112
    protected $studentrole;
113
 
114
    /** @var \stdClass  user role record. */
115
    protected $teacherrole;
116
 
117
    /**
118
     * Set up for every test
119
     */
120
    public function setUp(): void {
121
        global $DB;
1441 ariadna 122
        parent::setUp();
1 efrain 123
        $this->resetAfterTest();
124
        $this->setAdminUser();
125
 
126
        // Setup test data.
127
        $this->course = $this->getDataGenerator()->create_course();
128
        $this->quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $this->course->id]);
129
        $this->context = \context_module::instance($this->quiz->cmid);
130
        $this->cm = get_coursemodule_from_instance('quiz', $this->quiz->id);
131
 
132
        // Create users.
133
        $this->student = self::getDataGenerator()->create_user();
134
        $this->teacher = self::getDataGenerator()->create_user();
135
 
136
        // Users enrolments.
137
        $this->studentrole = $DB->get_record('role', ['shortname' => 'student']);
138
        $this->teacherrole = $DB->get_record('role', ['shortname' => 'editingteacher']);
139
        // Allow student to receive messages.
140
        $coursecontext = \context_course::instance($this->course->id);
141
        assign_capability('mod/quiz:emailnotifysubmission', CAP_ALLOW, $this->teacherrole->id, $coursecontext, true);
142
 
143
        $this->getDataGenerator()->enrol_user($this->student->id, $this->course->id, $this->studentrole->id, 'manual');
144
        $this->getDataGenerator()->enrol_user($this->teacher->id, $this->course->id, $this->teacherrole->id, 'manual');
145
    }
146
 
147
    /**
148
     * Create a quiz with questions including a started or finished attempt optionally
149
     *
150
     * @param  boolean $startattempt whether to start a new attempt
151
     * @param  boolean $finishattempt whether to finish the new attempt
152
     * @param  string $behaviour the quiz preferredbehaviour, defaults to 'deferredfeedback'.
153
     * @param  boolean $includeqattachments whether to include a question that supports attachments, defaults to false.
154
     * @param  array $extraoptions extra options for Quiz.
155
     * @return array array containing the quiz, context and the attempt
156
     */
157
    private function create_quiz_with_questions($startattempt = false, $finishattempt = false, $behaviour = 'deferredfeedback',
158
            $includeqattachments = false, $extraoptions = []) {
159
 
160
        // Create a new quiz with attempts.
161
        $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
162
        $data = ['course' => $this->course->id,
163
                      'sumgrades' => 2,
164
                      'preferredbehaviour' => $behaviour];
165
        $data = array_merge($data, $extraoptions);
166
        $quiz = $quizgenerator->create_instance($data);
167
        $context = \context_module::instance($quiz->cmid);
168
 
169
        // Create a couple of questions.
170
        $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
171
 
172
        $cat = $questiongenerator->create_question_category();
173
        $question = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]);
174
        quiz_add_quiz_question($question->id, $quiz);
175
        $question = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]);
176
        quiz_add_quiz_question($question->id, $quiz);
177
 
178
        if ($includeqattachments) {
179
            $question = $questiongenerator->create_question('essay', null, ['category' => $cat->id, 'attachments' => 1,
180
                'attachmentsrequired' => 1]);
181
            quiz_add_quiz_question($question->id, $quiz);
182
        }
183
 
184
        $quizobj = quiz_settings::create($quiz->id, $this->student->id);
185
 
186
        // Set grade to pass.
187
        $item = \grade_item::fetch(['courseid' => $this->course->id, 'itemtype' => 'mod',
188
                                        'itemmodule' => 'quiz', 'iteminstance' => $quiz->id, 'outcomeid' => null]);
189
        $item->gradepass = 80;
190
        $item->update();
191
 
192
        if ($startattempt or $finishattempt) {
193
            // Now, do one attempt.
194
            $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
195
            $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
196
 
197
            $timenow = time();
198
            $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $this->student->id);
199
            quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
200
            quiz_attempt_save_started($quizobj, $quba, $attempt);
201
            $attemptobj = quiz_attempt::create($attempt->id);
202
 
203
            if ($finishattempt) {
204
                // Process some responses from the student.
205
                $tosubmit = [1 => ['answer' => '3.14']];
206
                $attemptobj->process_submitted_actions(time(), false, $tosubmit);
207
 
208
                // Finish the attempt.
1441 ariadna 209
                $attemptobj->process_submit(time(), false);
210
                $attemptobj->process_grade_submission(time());
1 efrain 211
            }
212
            return [$quiz, $context, $quizobj, $attempt, $attemptobj, $quba];
213
        } else {
214
            return [$quiz, $context, $quizobj];
215
        }
216
 
217
    }
218
 
219
    /*
220
     * Test get quizzes by courses
221
     */
11 efrain 222
    public function test_mod_quiz_get_quizzes_by_courses(): void {
1 efrain 223
        global $DB;
224
 
225
        // Create additional course.
226
        $course2 = self::getDataGenerator()->create_course();
227
 
228
        // Second quiz.
229
        $record = new \stdClass();
230
        $record->course = $course2->id;
231
        $record->intro = '<button>Test with HTML allowed.</button>';
1441 ariadna 232
        $timeopen = time() - 1;
233
        $record->timeopen = $timeopen;
234
        $record->precreateattempts = 1;
1 efrain 235
        $quiz2 = self::getDataGenerator()->create_module('quiz', $record);
236
 
237
        // Execute real Moodle enrolment as we'll call unenrol() method on the instance later.
238
        $enrol = enrol_get_plugin('manual');
239
        $enrolinstances = enrol_get_instances($course2->id, true);
240
        foreach ($enrolinstances as $courseenrolinstance) {
241
            if ($courseenrolinstance->enrol == "manual") {
242
                $instance2 = $courseenrolinstance;
243
                break;
244
            }
245
        }
246
        $enrol->enrol_user($instance2, $this->student->id, $this->studentrole->id);
247
 
248
        self::setUser($this->student);
249
 
250
        $returndescription = mod_quiz_external::get_quizzes_by_courses_returns();
251
 
252
        // Create what we expect to be returned when querying the two courses.
253
        // First for the student user.
254
        $allusersfields = ['id', 'coursemodule', 'course', 'name', 'intro', 'introformat', 'introfiles', 'lang',
255
                                'timeopen', 'timeclose', 'grademethod', 'section', 'visible', 'groupmode', 'groupingid',
256
                                'attempts', 'timelimit', 'grademethod', 'decimalpoints', 'questiondecimalpoints', 'sumgrades',
257
                                'grade', 'preferredbehaviour', 'hasfeedback'];
258
        $userswithaccessfields = ['attemptonlast', 'reviewattempt', 'reviewcorrectness', 'reviewmaxmarks', 'reviewmarks',
259
                                        'reviewspecificfeedback', 'reviewgeneralfeedback', 'reviewrightanswer',
260
                                        'reviewoverallfeedback', 'questionsperpage', 'navmethod',
261
                                        'browsersecurity', 'delay1', 'delay2', 'showuserpicture', 'showblocks',
262
                                        'completionattemptsexhausted', 'completionpass', 'autosaveperiod', 'hasquestions',
263
                                        'overduehandling', 'graceperiod', 'canredoquestions', 'allowofflineattempts'];
1441 ariadna 264
        $managerfields = ['shuffleanswers', 'timecreated', 'timemodified', 'password', 'subnet', 'precreateattempts'];
1 efrain 265
 
266
        // Add expected coursemodule and other data.
267
        $quiz1 = $this->quiz;
268
        $quiz1->coursemodule = $quiz1->cmid;
269
        $quiz1->introformat = 1;
270
        $quiz1->section = 0;
271
        $quiz1->visible = true;
272
        $quiz1->groupmode = 0;
273
        $quiz1->groupingid = 0;
274
        $quiz1->hasquestions = 0;
275
        $quiz1->hasfeedback = 0;
276
        $quiz1->completionpass = 0;
277
        $quiz1->autosaveperiod = get_config('quiz', 'autosaveperiod');
278
        $quiz1->introfiles = [];
279
        $quiz1->lang = '';
280
 
281
        $quiz2->coursemodule = $quiz2->cmid;
282
        $quiz2->introformat = 1;
283
        $quiz2->section = 0;
284
        $quiz2->visible = true;
285
        $quiz2->groupmode = 0;
286
        $quiz2->groupingid = 0;
287
        $quiz2->hasquestions = 0;
288
        $quiz2->hasfeedback = 0;
289
        $quiz2->completionpass = 0;
290
        $quiz2->autosaveperiod = get_config('quiz', 'autosaveperiod');
291
        $quiz2->introfiles = [];
292
        $quiz2->lang = '';
293
 
294
        foreach (array_merge($allusersfields, $userswithaccessfields) as $field) {
295
            $expected1[$field] = $quiz1->{$field};
296
            $expected2[$field] = $quiz2->{$field};
297
        }
298
 
299
        $expectedquizzes = [$expected2, $expected1];
300
 
301
        // Call the external function passing course ids.
302
        $result = mod_quiz_external::get_quizzes_by_courses([$course2->id, $this->course->id]);
303
        $result = external_api::clean_returnvalue($returndescription, $result);
304
 
305
        $this->assertEquals($expectedquizzes, $result['quizzes']);
306
        $this->assertCount(0, $result['warnings']);
307
 
308
        // Call the external function without passing course id.
309
        $result = mod_quiz_external::get_quizzes_by_courses();
310
        $result = external_api::clean_returnvalue($returndescription, $result);
311
        $this->assertEquals($expectedquizzes, $result['quizzes']);
312
        $this->assertCount(0, $result['warnings']);
313
 
314
        // Unenrol user from second course and alter expected quizzes.
315
        $enrol->unenrol_user($instance2, $this->student->id);
316
        array_shift($expectedquizzes);
317
 
318
        // Call the external function without passing course id.
319
        $result = mod_quiz_external::get_quizzes_by_courses();
320
        $result = external_api::clean_returnvalue($returndescription, $result);
321
        $this->assertEquals($expectedquizzes, $result['quizzes']);
322
 
323
        // Call for the second course we unenrolled the user from, expected warning.
324
        $result = mod_quiz_external::get_quizzes_by_courses([$course2->id]);
325
        $this->assertCount(1, $result['warnings']);
326
        $this->assertEquals('1', $result['warnings'][0]['warningcode']);
327
        $this->assertEquals($course2->id, $result['warnings'][0]['itemid']);
328
 
329
        // Now, try as a teacher for getting all the additional fields.
330
        self::setUser($this->teacher);
331
 
332
        foreach ($managerfields as $field) {
333
            $expectedquizzes[0][$field] = $quiz1->{$field};
334
        }
335
 
336
        $result = mod_quiz_external::get_quizzes_by_courses();
337
        $result = external_api::clean_returnvalue($returndescription, $result);
338
        $this->assertEquals($expectedquizzes, $result['quizzes']);
339
 
340
        // Admin also should get all the information.
341
        self::setAdminUser();
342
 
343
        $result = mod_quiz_external::get_quizzes_by_courses([$this->course->id]);
344
        $result = external_api::clean_returnvalue($returndescription, $result);
345
        $this->assertEquals($expectedquizzes, $result['quizzes']);
346
 
347
        // Now, prevent access.
348
        $enrol->enrol_user($instance2, $this->student->id);
349
 
350
        self::setUser($this->student);
351
 
352
        $quiz2->timeclose = time() - DAYSECS;
353
        $DB->update_record('quiz', $quiz2);
354
 
355
        $result = mod_quiz_external::get_quizzes_by_courses();
356
        $result = external_api::clean_returnvalue($returndescription, $result);
357
        $this->assertCount(2, $result['quizzes']);
358
        // We only see a limited set of fields.
359
        $this->assertCount(5, $result['quizzes'][0]);
360
        $this->assertEquals($quiz2->id, $result['quizzes'][0]['id']);
361
        $this->assertEquals($quiz2->cmid, $result['quizzes'][0]['coursemodule']);
362
        $this->assertEquals($quiz2->course, $result['quizzes'][0]['course']);
363
        $this->assertEquals($quiz2->name, $result['quizzes'][0]['name']);
364
        $this->assertEquals($quiz2->course, $result['quizzes'][0]['course']);
365
 
366
        $this->assertFalse(isset($result['quizzes'][0]['timelimit']));
367
 
368
    }
369
 
370
    /**
371
     * Test test_view_quiz
372
     */
11 efrain 373
    public function test_view_quiz(): void {
1 efrain 374
        global $DB;
375
 
376
        // Test invalid instance id.
377
        try {
378
            mod_quiz_external::view_quiz(0);
379
            $this->fail('Exception expected due to invalid mod_quiz instance id.');
380
        } catch (moodle_exception $e) {
381
            $this->assertEquals('invalidrecord', $e->errorcode);
382
        }
383
 
384
        // Test not-enrolled user.
385
        $usernotenrolled = self::getDataGenerator()->create_user();
386
        $this->setUser($usernotenrolled);
387
        try {
388
            mod_quiz_external::view_quiz($this->quiz->id);
389
            $this->fail('Exception expected due to not enrolled user.');
390
        } catch (moodle_exception $e) {
391
            $this->assertEquals('requireloginerror', $e->errorcode);
392
        }
393
 
394
        // Test user with full capabilities.
395
        $this->setUser($this->student);
396
 
397
        // Trigger and capture the event.
398
        $sink = $this->redirectEvents();
399
 
400
        $result = mod_quiz_external::view_quiz($this->quiz->id);
401
        $result = external_api::clean_returnvalue(mod_quiz_external::view_quiz_returns(), $result);
402
        $this->assertTrue($result['status']);
403
 
404
        $events = $sink->get_events();
405
        $this->assertCount(1, $events);
406
        $event = array_shift($events);
407
 
408
        // Checking that the event contains the expected values.
409
        $this->assertInstanceOf('\mod_quiz\event\course_module_viewed', $event);
410
        $this->assertEquals($this->context, $event->get_context());
411
        $moodlequiz = new \moodle_url('/mod/quiz/view.php', ['id' => $this->cm->id]);
412
        $this->assertEquals($moodlequiz, $event->get_url());
413
        $this->assertEventContextNotUsed($event);
414
        $this->assertNotEmpty($event->get_name());
415
 
416
        // Test user with no capabilities.
417
        // We need a explicit prohibit since this capability is only defined in authenticated user and guest roles.
418
        assign_capability('mod/quiz:view', CAP_PROHIBIT, $this->studentrole->id, $this->context->id);
419
        // Empty all the caches that may be affected  by this change.
420
        accesslib_clear_all_caches_for_unit_testing();
421
        \course_modinfo::clear_instance_cache();
422
 
423
        try {
424
            mod_quiz_external::view_quiz($this->quiz->id);
425
            $this->fail('Exception expected due to missing capability.');
426
        } catch (moodle_exception $e) {
427
            $this->assertEquals('requireloginerror', $e->errorcode);
428
        }
429
 
430
    }
431
 
1441 ariadna 432
    /**
433
     * Test get_user_attempts
434
     *
435
     * @todo Remove in Moodle 6.0 as part of MDL-80956 final deprecations.
436
     */
1 efrain 437
    public function test_get_user_attempts(): void {
438
 
439
        // Create a quiz with one attempt finished.
440
        [$quiz, $context, $quizobj, $attempt, $attemptobj] = $this->create_quiz_with_questions(true, true);
441
 
442
        $this->setUser($this->student);
443
        $result = mod_quiz_external::get_user_attempts($quiz->id);
1441 ariadna 444
        $this->assertDebuggingCalled();
1 efrain 445
        $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
446
 
447
        $this->assertCount(1, $result['attempts']);
448
        $this->assertEquals($attempt->id, $result['attempts'][0]['id']);
449
        $this->assertEquals($quiz->id, $result['attempts'][0]['quiz']);
450
        $this->assertEquals($this->student->id, $result['attempts'][0]['userid']);
451
        $this->assertEquals(1, $result['attempts'][0]['attempt']);
452
        $this->assertArrayHasKey('sumgrades', $result['attempts'][0]);
453
        $this->assertEquals(1.0, $result['attempts'][0]['sumgrades']);
1441 ariadna 454
        $this->assertEquals(quiz_attempt::FINISHED, $result['attempts'][0]['state']);
1 efrain 455
 
456
        // Test filters. Only finished.
1441 ariadna 457
        $this->resetDebugging();
1 efrain 458
        $result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'finished', false);
459
        $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
1441 ariadna 460
        $this->assertDebuggingCalled();
1 efrain 461
 
462
        $this->assertCount(1, $result['attempts']);
463
        $this->assertEquals($attempt->id, $result['attempts'][0]['id']);
464
 
465
        // Test filters. All attempts.
1441 ariadna 466
        $this->resetDebugging();
1 efrain 467
        $result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'all', false);
468
        $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
1441 ariadna 469
        $this->assertDebuggingCalled();
1 efrain 470
 
471
        $this->assertCount(1, $result['attempts']);
472
        $this->assertEquals($attempt->id, $result['attempts'][0]['id']);
473
 
474
        // Test filters. Unfinished.
1441 ariadna 475
        $this->resetDebugging();
1 efrain 476
        $result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'unfinished', false);
477
        $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
1441 ariadna 478
        $this->assertDebuggingCalled();
1 efrain 479
 
480
        $this->assertCount(0, $result['attempts']);
481
 
482
        // Start a new attempt, but not finish it.
483
        $timenow = time();
484
        $attempt = quiz_create_attempt($quizobj, 2, false, $timenow, false, $this->student->id);
485
        $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
486
        $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
487
 
488
        quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
489
        quiz_attempt_save_started($quizobj, $quba, $attempt);
490
 
491
        // Test filters. All attempts.
1441 ariadna 492
        $this->resetDebugging();
1 efrain 493
        $result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'all', false);
494
        $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
1441 ariadna 495
        $this->assertDebuggingCalled();
1 efrain 496
 
497
        $this->assertCount(2, $result['attempts']);
498
 
499
        // Test filters. Unfinished.
1441 ariadna 500
        $this->resetDebugging();
1 efrain 501
        $result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'unfinished', false);
502
        $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
1441 ariadna 503
        $this->assertDebuggingCalled();
1 efrain 504
 
505
        $this->assertCount(1, $result['attempts']);
506
 
507
        // Test manager can see user attempts.
508
        $this->setUser($this->teacher);
1441 ariadna 509
        $this->resetDebugging();
1 efrain 510
        $result = mod_quiz_external::get_user_attempts($quiz->id, $this->student->id);
511
        $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
1441 ariadna 512
        $this->assertDebuggingCalled();
1 efrain 513
 
514
        $this->assertCount(1, $result['attempts']);
515
        $this->assertEquals($this->student->id, $result['attempts'][0]['userid']);
516
 
1441 ariadna 517
        $this->resetDebugging();
1 efrain 518
        $result = mod_quiz_external::get_user_attempts($quiz->id, $this->student->id, 'all');
519
        $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
1441 ariadna 520
        $this->assertDebuggingCalled();
1 efrain 521
 
522
        $this->assertCount(2, $result['attempts']);
523
        $this->assertEquals($this->student->id, $result['attempts'][0]['userid']);
524
 
525
        // Invalid parameters.
526
        try {
1441 ariadna 527
            $this->resetDebugging();
1 efrain 528
            mod_quiz_external::get_user_attempts($quiz->id, $this->student->id, 'INVALID_PARAMETER');
529
            $this->fail('Exception expected due to missing capability.');
530
        } catch (\invalid_parameter_exception $e) {
1441 ariadna 531
            $this->assertDebuggingCalled();
1 efrain 532
            $this->assertEquals('invalidparameter', $e->errorcode);
533
        }
534
    }
535
 
1441 ariadna 536
    /**
537
     * Test get_user_attempts with extra grades
538
     *
539
     * @todo Remove in Moodle 6.0 as part of MDL-80956 final deprecations.
540
     */
1 efrain 541
    public function test_get_user_attempts_with_extra_grades(): void {
542
        global $DB;
543
 
544
        // Create a quiz with one attempt finished.
545
        [$quiz, , , $attempt, $attemptobj] = $this->create_quiz_with_questions(true, true);
546
 
547
        // Add some extra grade items.
548
        $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
549
        $listeninggrade = $quizgenerator->create_grade_item(['quizid' => $attemptobj->get_quizid(), 'name' => 'Listening']);
550
        $readinggrade = $quizgenerator->create_grade_item(['quizid' => $attemptobj->get_quizid(), 'name' => 'Reading']);
551
        $structure = $attemptobj->get_quizobj()->get_structure();
552
        $structure->update_slot_grade_item($structure->get_slot_by_number(1), $listeninggrade->id);
553
        $structure->update_slot_grade_item($structure->get_slot_by_number(2), $readinggrade->id);
554
 
555
        $this->setUser($this->student);
1441 ariadna 556
        $this->resetDebugging();
1 efrain 557
        $result = mod_quiz_external::get_user_attempts($quiz->id);
558
        $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
1441 ariadna 559
        $this->assertDebuggingCalled();
1 efrain 560
 
561
        $this->assertCount(1, $result['attempts']);
562
        $this->assertEquals($attempt->id, $result['attempts'][0]['id']);
563
 
564
        // Verify additional grades.
565
        $this->assertEquals(['name' => 'Listening', 'grade' => 1, 'maxgrade' => 1], $result['attempts'][0]['gradeitemmarks'][0]);
566
        $this->assertEquals(['name' => 'Reading', 'grade' => 0, 'maxgrade' => 1], $result['attempts'][0]['gradeitemmarks'][1]);
567
 
568
        // Now change the review options, so marks are not displayed, and check the result.
569
        $DB->set_field('quiz', 'reviewmarks', 0, ['id' => $quiz->id]);
1441 ariadna 570
        $this->resetDebugging();
1 efrain 571
        $result = mod_quiz_external::get_user_attempts($quiz->id);
572
        $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
1441 ariadna 573
        $this->assertDebuggingCalled();
1 efrain 574
 
575
        $this->assertCount(1, $result['attempts']);
576
        $this->assertEquals($attempt->id, $result['attempts'][0]['id']);
577
        $this->assertArrayNotHasKey('gradeitemmarks', $result['attempts'][0]);
578
    }
579
 
580
    /**
581
     * Test get_user_attempts with marks hidden
1441 ariadna 582
     *
583
     * @todo Remove in Moodle 6.0 as part of MDL-80956 final deprecations.
1 efrain 584
     */
11 efrain 585
    public function test_get_user_attempts_with_marks_hidden(): void {
1 efrain 586
        // Create quiz with one attempt finished and hide the mark.
587
        list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(
588
                true, true, 'deferredfeedback', false,
589
                ['marksduring' => 0, 'marksimmediately' => 0, 'marksopen' => 0, 'marksclosed' => 0]);
590
 
591
        // Student cannot see the grades.
592
        $this->setUser($this->student);
1441 ariadna 593
        $this->resetDebugging();
1 efrain 594
        $result = mod_quiz_external::get_user_attempts($quiz->id);
595
        $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
1441 ariadna 596
        $this->assertDebuggingCalled();
1 efrain 597
 
598
        $this->assertCount(1, $result['attempts']);
599
        $this->assertEquals($attempt->id, $result['attempts'][0]['id']);
600
        $this->assertEquals($quiz->id, $result['attempts'][0]['quiz']);
601
        $this->assertEquals($this->student->id, $result['attempts'][0]['userid']);
602
        $this->assertEquals(1, $result['attempts'][0]['attempt']);
603
        $this->assertArrayHasKey('sumgrades', $result['attempts'][0]);
604
        $this->assertEquals(null, $result['attempts'][0]['sumgrades']);
605
 
606
        // Test manager can see user grades.
607
        $this->setUser($this->teacher);
1441 ariadna 608
        $this->resetDebugging();
1 efrain 609
        $result = mod_quiz_external::get_user_attempts($quiz->id, $this->student->id);
610
        $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
1441 ariadna 611
        $this->assertDebuggingCalled();
1 efrain 612
 
613
        $this->assertCount(1, $result['attempts']);
614
        $this->assertEquals($attempt->id, $result['attempts'][0]['id']);
615
        $this->assertEquals($quiz->id, $result['attempts'][0]['quiz']);
616
        $this->assertEquals($this->student->id, $result['attempts'][0]['userid']);
617
        $this->assertEquals(1, $result['attempts'][0]['attempt']);
618
        $this->assertArrayHasKey('sumgrades', $result['attempts'][0]);
619
        $this->assertEquals(1.0, $result['attempts'][0]['sumgrades']);
620
    }
621
 
622
    /**
1441 ariadna 623
     * Test get_user_attempts when the attempt is in 'submitted' state.
624
     *
625
     * @todo Remove in Moodle 6.0 as part of MDL-80956 final deprecations.
626
     * @covers \mod_quiz_external::get_user_attempts
627
     */
628
    public function test_get_user_attempts_submitted(): void {
629
 
630
        // Create a quiz with one attempt.
631
        [$quiz, , , $attempt, $attemptobj] = $this->create_quiz_with_questions(true);
632
        // Submit the attempt but do not finish it.
633
        // Process some responses from the student.
634
        $tosubmit = [1 => ['answer' => '3.14']];
635
        $attemptobj->process_submitted_actions(time(), false, $tosubmit);
636
        $attemptobj->process_submit(time(), false);
637
 
638
        $this->setUser($this->student);
639
        $result = mod_quiz_external::get_user_attempts($quiz->id);
640
        $this->assertDebuggingCalled();
641
        $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
642
 
643
        $this->assertCount(1, $result['attempts']);
644
        $this->assertEquals($attempt->id, $result['attempts'][0]['id']);
645
        $this->assertEquals($quiz->id, $result['attempts'][0]['quiz']);
646
        $this->assertEquals($this->student->id, $result['attempts'][0]['userid']);
647
        $this->assertEquals(1, $result['attempts'][0]['attempt']);
648
        $this->assertArrayHasKey('sumgrades', $result['attempts'][0]);
649
        $this->assertNull($result['attempts'][0]['sumgrades']); // No grades.
650
        $this->assertEquals(quiz_attempt::FINISHED, $result['attempts'][0]['state']); // State is returned as finished.
651
    }
652
 
653
    /**
654
     * Test get_user_attempts when the attempt is in 'notstarted' state. The attempt should not be returned.
655
     *
656
     * @todo Remove in Moodle 6.0 as part of MDL-80956 final deprecations.
657
     * @covers \mod_quiz_external::get_user_attempts
658
     */
659
    public function test_get_user_attempts_notstarted(): void {
660
        // Create a quiz.
661
        [$quiz, , $quizobj, , ] = $this->create_quiz_with_questions();
662
        // Create an attempt but do not start it.
663
        // Now, do one attempt.
664
        $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
665
        $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
666
 
667
        $timenow = time();
668
        $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $this->student->id);
669
        quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
670
        quiz_attempt_save_not_started($quba, $attempt);
671
 
672
        $this->setUser($this->student);
673
        $result = mod_quiz_external::get_user_attempts($quiz->id, $this->student->id, 'all');
674
        $this->assertDebuggingCalled();
675
        $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
676
 
677
        $this->assertCount(0, $result['attempts']);
678
    }
679
 
680
    /**
681
     * Test get_quiz_user_attempts
682
     *
683
     * @covers \mod_quiz_external::get_user_quiz_attempts
684
     */
685
    public function test_get_user_quiz_attempts(): void {
686
 
687
        // Create a quiz with one attempt finished.
688
        [$quiz, , $quizobj, $attempt, ] = $this->create_quiz_with_questions(true, true);
689
 
690
        $this->setUser($this->student);
691
        $result = mod_quiz_external::get_user_quiz_attempts($quiz->id);
692
        $result = external_api::clean_returnvalue(mod_quiz_external::get_user_quiz_attempts_returns(), $result);
693
 
694
        $this->assertCount(1, $result['attempts']);
695
        $this->assertEquals($attempt->id, $result['attempts'][0]['id']);
696
        $this->assertEquals($quiz->id, $result['attempts'][0]['quiz']);
697
        $this->assertEquals($this->student->id, $result['attempts'][0]['userid']);
698
        $this->assertEquals(1, $result['attempts'][0]['attempt']);
699
        $this->assertArrayHasKey('sumgrades', $result['attempts'][0]);
700
        $this->assertEquals(1.0, $result['attempts'][0]['sumgrades']);
701
        $this->assertEquals(quiz_attempt::FINISHED, $result['attempts'][0]['state']);
702
 
703
        // Test filters. Only finished.
704
        $result = mod_quiz_external::get_user_quiz_attempts($quiz->id, 0, 'finished', false);
705
        $result = external_api::clean_returnvalue(mod_quiz_external::get_user_quiz_attempts_returns(), $result);
706
 
707
        $this->assertCount(1, $result['attempts']);
708
        $this->assertEquals($attempt->id, $result['attempts'][0]['id']);
709
 
710
        // Test filters. All attempts.
711
        $result = mod_quiz_external::get_user_quiz_attempts($quiz->id, 0, 'all', false);
712
        $result = external_api::clean_returnvalue(mod_quiz_external::get_user_quiz_attempts_returns(), $result);
713
 
714
        $this->assertCount(1, $result['attempts']);
715
        $this->assertEquals($attempt->id, $result['attempts'][0]['id']);
716
 
717
        // Test filters. Unfinished.
718
        $result = mod_quiz_external::get_user_quiz_attempts($quiz->id, 0, 'unfinished', false);
719
        $result = external_api::clean_returnvalue(mod_quiz_external::get_user_quiz_attempts_returns(), $result);
720
 
721
        $this->assertCount(0, $result['attempts']);
722
 
723
        // Start a new attempt, but not finish it.
724
        $timenow = time();
725
        $attempt = quiz_create_attempt($quizobj, 2, false, $timenow, false, $this->student->id);
726
        $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
727
        $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
728
 
729
        quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
730
        quiz_attempt_save_started($quizobj, $quba, $attempt);
731
 
732
        // Test filters. All attempts.
733
        $result = mod_quiz_external::get_user_quiz_attempts($quiz->id, 0, 'all', false);
734
        $result = external_api::clean_returnvalue(mod_quiz_external::get_user_quiz_attempts_returns(), $result);
735
 
736
        $this->assertCount(2, $result['attempts']);
737
 
738
        // Test filters. Unfinished.
739
        $result = mod_quiz_external::get_user_quiz_attempts($quiz->id, 0, 'unfinished', false);
740
        $result = external_api::clean_returnvalue(mod_quiz_external::get_user_quiz_attempts_returns(), $result);
741
 
742
        $this->assertCount(1, $result['attempts']);
743
 
744
        // Test manager can see user attempts.
745
        $this->setUser($this->teacher);
746
        $result = mod_quiz_external::get_user_quiz_attempts($quiz->id, $this->student->id);
747
        $result = external_api::clean_returnvalue(mod_quiz_external::get_user_quiz_attempts_returns(), $result);
748
 
749
        $this->assertCount(1, $result['attempts']);
750
        $this->assertEquals($this->student->id, $result['attempts'][0]['userid']);
751
 
752
        $result = mod_quiz_external::get_user_quiz_attempts($quiz->id, $this->student->id, 'all');
753
        $result = external_api::clean_returnvalue(mod_quiz_external::get_user_quiz_attempts_returns(), $result);
754
 
755
        $this->assertCount(2, $result['attempts']);
756
        $this->assertEquals($this->student->id, $result['attempts'][0]['userid']);
757
 
758
        // Invalid parameters.
759
        try {
760
            mod_quiz_external::get_user_quiz_attempts($quiz->id, $this->student->id, 'INVALID_PARAMETER');
761
            $this->fail('Exception expected due to missing capability.');
762
        } catch (\invalid_parameter_exception $e) {
763
            $this->assertEquals('invalidparameter', $e->errorcode);
764
        }
765
    }
766
 
767
    /**
768
     * Test get_user_quiz_attempts with extra grades
769
     */
770
    public function test_get_user_quiz_attempts_with_extra_grades(): void {
771
        global $DB;
772
 
773
        // Create a quiz with one attempt finished.
774
        [$quiz, , , $attempt, $attemptobj] = $this->create_quiz_with_questions(true, true);
775
 
776
        // Add some extra grade items.
777
        $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
778
        $listeninggrade = $quizgenerator->create_grade_item(['quizid' => $attemptobj->get_quizid(), 'name' => 'Listening']);
779
        $readinggrade = $quizgenerator->create_grade_item(['quizid' => $attemptobj->get_quizid(), 'name' => 'Reading']);
780
        $structure = $attemptobj->get_quizobj()->get_structure();
781
        $structure->update_slot_grade_item($structure->get_slot_by_number(1), $listeninggrade->id);
782
        $structure->update_slot_grade_item($structure->get_slot_by_number(2), $readinggrade->id);
783
 
784
        $this->setUser($this->student);
785
        $result = mod_quiz_external::get_user_quiz_attempts($quiz->id);
786
        $result = external_api::clean_returnvalue(mod_quiz_external::get_user_quiz_attempts_returns(), $result);
787
 
788
        $this->assertCount(1, $result['attempts']);
789
        $this->assertEquals($attempt->id, $result['attempts'][0]['id']);
790
 
791
        // Verify additional grades.
792
        $this->assertEquals(['name' => 'Listening', 'grade' => 1, 'maxgrade' => 1], $result['attempts'][0]['gradeitemmarks'][0]);
793
        $this->assertEquals(['name' => 'Reading', 'grade' => 0, 'maxgrade' => 1], $result['attempts'][0]['gradeitemmarks'][1]);
794
 
795
        // Now change the review options, so marks are not displayed, and check the result.
796
        $DB->set_field('quiz', 'reviewmarks', 0, ['id' => $quiz->id]);
797
        $result = mod_quiz_external::get_user_quiz_attempts($quiz->id);
798
        $result = external_api::clean_returnvalue(mod_quiz_external::get_user_quiz_attempts_returns(), $result);
799
 
800
        $this->assertCount(1, $result['attempts']);
801
        $this->assertEquals($attempt->id, $result['attempts'][0]['id']);
802
        $this->assertArrayNotHasKey('gradeitemmarks', $result['attempts'][0]);
803
    }
804
 
805
    /**
806
     * Test get_user_quiz_attempts with marks hidden
807
     *
808
     * @covers \mod_quiz_external::get_user_quiz_attempts
809
     */
810
    public function test_get_user_quiz_attempts_with_marks_hidden(): void {
811
        // Create quiz with one attempt finished and hide the mark.
812
        [$quiz, , , $attempt, ] = $this->create_quiz_with_questions(
813
                true, true, 'deferredfeedback', false,
814
                ['marksduring' => 0, 'marksimmediately' => 0, 'marksopen' => 0, 'marksclosed' => 0]);
815
 
816
        // Student cannot see the grades.
817
        $this->setUser($this->student);
818
        $result = mod_quiz_external::get_user_quiz_attempts($quiz->id);
819
        $result = external_api::clean_returnvalue(mod_quiz_external::get_user_quiz_attempts_returns(), $result);
820
 
821
        $this->assertCount(1, $result['attempts']);
822
        $this->assertEquals($attempt->id, $result['attempts'][0]['id']);
823
        $this->assertEquals($quiz->id, $result['attempts'][0]['quiz']);
824
        $this->assertEquals($this->student->id, $result['attempts'][0]['userid']);
825
        $this->assertEquals(1, $result['attempts'][0]['attempt']);
826
        $this->assertArrayHasKey('sumgrades', $result['attempts'][0]);
827
        $this->assertEquals(null, $result['attempts'][0]['sumgrades']);
828
 
829
        // Test manager can see user grades.
830
        $this->setUser($this->teacher);
831
        $result = mod_quiz_external::get_user_quiz_attempts($quiz->id, $this->student->id);
832
        $result = external_api::clean_returnvalue(mod_quiz_external::get_user_quiz_attempts_returns(), $result);
833
 
834
        $this->assertCount(1, $result['attempts']);
835
        $this->assertEquals($attempt->id, $result['attempts'][0]['id']);
836
        $this->assertEquals($quiz->id, $result['attempts'][0]['quiz']);
837
        $this->assertEquals($this->student->id, $result['attempts'][0]['userid']);
838
        $this->assertEquals(1, $result['attempts'][0]['attempt']);
839
        $this->assertArrayHasKey('sumgrades', $result['attempts'][0]);
840
        $this->assertEquals(1.0, $result['attempts'][0]['sumgrades']);
841
    }
842
 
843
    /**
844
     * Test get_user_quiz_attempts when the attempt is in 'submitted' state.
845
     *
846
     * @covers \mod_quiz_external::get_user_quiz_attempts
847
     */
848
    public function test_get_user_quiz_attempts_submitted(): void {
849
 
850
        // Create a quiz with one attempt.
851
        [$quiz, , , $attempt, $attemptobj] = $this->create_quiz_with_questions(true);
852
        // Submit the attempt but do not finish it.
853
        // Process some responses from the student.
854
        $tosubmit = [1 => ['answer' => '3.14']];
855
        $attemptobj->process_submitted_actions(time(), false, $tosubmit);
856
        $attemptobj->process_submit(time(), false);
857
 
858
        $this->setUser($this->student);
859
        $result = mod_quiz_external::get_user_quiz_attempts($quiz->id);
860
        $result = external_api::clean_returnvalue(mod_quiz_external::get_user_quiz_attempts_returns(), $result);
861
 
862
        $this->assertCount(1, $result['attempts']);
863
        $this->assertEquals($attempt->id, $result['attempts'][0]['id']);
864
        $this->assertEquals($quiz->id, $result['attempts'][0]['quiz']);
865
        $this->assertEquals($this->student->id, $result['attempts'][0]['userid']);
866
        $this->assertEquals(1, $result['attempts'][0]['attempt']);
867
        $this->assertArrayHasKey('sumgrades', $result['attempts'][0]);
868
        $this->assertNull($result['attempts'][0]['sumgrades']); // No grades.
869
        $this->assertEquals(quiz_attempt::SUBMITTED, $result['attempts'][0]['state']);
870
    }
871
 
872
    /**
873
     * Test get_user_quiz_attempts when the attempt is in 'notstarted' state.
874
     *
875
     * @covers \mod_quiz_external::get_user_quiz_attempts
876
     */
877
    public function test_get_user_quiz_attempts_notstarted(): void {
878
        // Create a quiz.
879
        [$quiz, , $quizobj, , ] = $this->create_quiz_with_questions();
880
        // Create an attempt but do not start it.
881
        // Now, do one attempt.
882
        $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
883
        $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
884
 
885
        $timenow = time();
886
        $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $this->student->id);
887
        quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
888
        quiz_attempt_save_not_started($quba, $attempt);
889
 
890
        $this->setUser($this->student);
891
        $result = mod_quiz_external::get_user_quiz_attempts($quiz->id, $this->student->id, 'all');
892
        $result = external_api::clean_returnvalue(mod_quiz_external::get_user_quiz_attempts_returns(), $result);
893
 
894
        $this->assertCount(1, $result['attempts']);
895
        $this->assertEquals($attempt->id, $result['attempts'][0]['id']);
896
        $this->assertEquals($quiz->id, $result['attempts'][0]['quiz']);
897
        $this->assertEquals($this->student->id, $result['attempts'][0]['userid']);
898
        $this->assertEquals(1, $result['attempts'][0]['attempt']);
899
        $this->assertArrayHasKey('sumgrades', $result['attempts'][0]);
900
        $this->assertNull($result['attempts'][0]['sumgrades']);
901
        $this->assertEquals(quiz_attempt::NOT_STARTED, $result['attempts'][0]['state']);
902
    }
903
 
904
    /**
1 efrain 905
     * Test get_user_best_grade
906
     */
11 efrain 907
    public function test_get_user_best_grade(): void {
1 efrain 908
        $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
909
        $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
910
        $questioncat = $questiongenerator->create_question_category();
911
 
912
        // Create a new quiz.
913
        $quizapi1 = $quizgenerator->create_instance([
914
                'name' => 'Test Quiz API 1',
915
                'course' => $this->course->id,
916
                'sumgrades' => 1
917
        ]);
918
        $quizapi2 = $quizgenerator->create_instance([
919
                'name' => 'Test Quiz API 2',
920
                'course' => $this->course->id,
921
                'sumgrades' => 1,
922
                'marksduring' => 0,
923
                'marksimmediately' => 0,
924
                'marksopen' => 0,
925
                'marksclosed' => 0
926
        ]);
927
 
928
        // Create a question.
929
        $question = $questiongenerator->create_question('numerical', null, ['category' => $questioncat->id]);
930
 
931
        // Add question to the quizzes.
932
        quiz_add_quiz_question($question->id, $quizapi1);
933
        quiz_add_quiz_question($question->id, $quizapi2);
934
 
935
        // Create quiz object.
936
        $quizapiobj1 = quiz_settings::create($quizapi1->id, $this->student->id);
937
        $quizapiobj2 = quiz_settings::create($quizapi2->id, $this->student->id);
938
 
939
        // Set grade to pass.
940
        $item = \grade_item::fetch([
941
                'courseid' => $this->course->id,
942
                'itemtype' => 'mod',
943
                'itemmodule' => 'quiz',
944
                'iteminstance' => $quizapi1->id,
945
                'outcomeid' => null
946
        ]);
947
        $item->gradepass = 80;
948
        $item->update();
949
 
950
        $item = \grade_item::fetch([
951
                'courseid' => $this->course->id,
952
                'itemtype' => 'mod',
953
                'itemmodule' => 'quiz',
954
                'iteminstance' => $quizapi2->id,
955
                'outcomeid' => null
956
        ]);
957
        $item->gradepass = 80;
958
        $item->update();
959
 
960
        // Start the passing attempt.
961
        $quba1 = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizapiobj1->get_context());
962
        $quba1->set_preferred_behaviour($quizapiobj1->get_quiz()->preferredbehaviour);
963
 
964
        $quba2 = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizapiobj2->get_context());
965
        $quba2->set_preferred_behaviour($quizapiobj2->get_quiz()->preferredbehaviour);
966
 
967
        // Start the testing for quizapi1 that allow the student to view the grade.
968
 
969
        $this->setUser($this->student);
970
        $result = mod_quiz_external::get_user_best_grade($quizapi1->id);
971
        $result = external_api::clean_returnvalue(mod_quiz_external::get_user_best_grade_returns(), $result);
972
 
973
        // No grades yet.
974
        $this->assertFalse($result['hasgrade']);
975
        $this->assertTrue(!isset($result['grade']));
976
 
977
        // Start the attempt.
978
        $timenow = time();
979
        $attempt = quiz_create_attempt($quizapiobj1, 1, false, $timenow, false, $this->student->id);
980
        quiz_start_new_attempt($quizapiobj1, $quba1, $attempt, 1, $timenow);
981
        quiz_attempt_save_started($quizapiobj1, $quba1, $attempt);
982
 
983
        // Process some responses from the student.
984
        $attemptobj = quiz_attempt::create($attempt->id);
985
        $attemptobj->process_submitted_actions($timenow, false, [1 => ['answer' => '3.14']]);
986
 
987
        // Finish the attempt.
1441 ariadna 988
        $attemptobj->process_submit($timenow, false);
989
        $attemptobj->process_grade_submission($timenow);
1 efrain 990
 
991
        $result = mod_quiz_external::get_user_best_grade($quizapi1->id);
992
        $result = external_api::clean_returnvalue(mod_quiz_external::get_user_best_grade_returns(), $result);
993
 
994
        // Now I have grades.
995
        $this->assertTrue($result['hasgrade']);
996
        $this->assertEquals(100.0, $result['grade']);
997
        $this->assertEquals(80, $result['gradetopass']);
998
 
999
        // We should not see other users grades.
1000
        $anotherstudent = self::getDataGenerator()->create_user();
1001
        $this->getDataGenerator()->enrol_user($anotherstudent->id, $this->course->id, $this->studentrole->id, 'manual');
1002
 
1003
        try {
1004
            mod_quiz_external::get_user_best_grade($quizapi1->id, $anotherstudent->id);
1005
            $this->fail('Exception expected due to missing capability.');
1006
        } catch (\required_capability_exception $e) {
1007
            $this->assertEquals('nopermissions', $e->errorcode);
1008
        }
1009
 
1010
        // Teacher must be able to see student grades.
1011
        $this->setUser($this->teacher);
1012
 
1013
        $result = mod_quiz_external::get_user_best_grade($quizapi1->id, $this->student->id);
1014
        $result = external_api::clean_returnvalue(mod_quiz_external::get_user_best_grade_returns(), $result);
1015
 
1016
        $this->assertTrue($result['hasgrade']);
1017
        $this->assertEquals(100.0, $result['grade']);
1018
        $this->assertEquals(80, $result['gradetopass']);
1019
 
1020
        // Invalid user.
1021
        try {
1022
            mod_quiz_external::get_user_best_grade($this->quiz->id, -1);
1023
            $this->fail('Exception expected due to missing capability.');
1024
        } catch (\dml_missing_record_exception $e) {
1025
            $this->assertEquals('invaliduser', $e->errorcode);
1026
        }
1027
 
1028
        // End the testing for quizapi1 that allow the student to view the grade.
1029
 
1030
        // Start the testing for quizapi2 that do not allow the student to view the grade.
1031
 
1032
        $this->setUser($this->student);
1033
        $result = mod_quiz_external::get_user_best_grade($quizapi2->id);
1034
        $result = external_api::clean_returnvalue(mod_quiz_external::get_user_best_grade_returns(), $result);
1035
 
1036
        // No grades yet.
1037
        $this->assertFalse($result['hasgrade']);
1038
        $this->assertTrue(!isset($result['grade']));
1039
 
1040
        // Start the attempt.
1041
        $timenow = time();
1042
        $attempt = quiz_create_attempt($quizapiobj2, 1, false, $timenow, false, $this->student->id);
1043
        quiz_start_new_attempt($quizapiobj2, $quba2, $attempt, 1, $timenow);
1044
        quiz_attempt_save_started($quizapiobj2, $quba2, $attempt);
1045
 
1046
        // Process some responses from the student.
1047
        $attemptobj = quiz_attempt::create($attempt->id);
1048
        $attemptobj->process_submitted_actions($timenow, false, [1 => ['answer' => '3.14']]);
1049
 
1050
        // Finish the attempt.
1441 ariadna 1051
        $attemptobj->process_submit($timenow, false);
1052
        $attemptobj->process_grade_submission($timenow);
1 efrain 1053
 
1054
        $result = mod_quiz_external::get_user_best_grade($quizapi2->id);
1055
        $result = external_api::clean_returnvalue(mod_quiz_external::get_user_best_grade_returns(), $result);
1056
 
1057
        // Now I have grades but I will not be allowed to see it.
1058
        $this->assertFalse($result['hasgrade']);
1059
        $this->assertTrue(!isset($result['grade']));
1060
 
1061
        // Teacher must be able to see student grades.
1062
        $this->setUser($this->teacher);
1063
 
1064
        $result = mod_quiz_external::get_user_best_grade($quizapi2->id, $this->student->id);
1065
        $result = external_api::clean_returnvalue(mod_quiz_external::get_user_best_grade_returns(), $result);
1066
 
1067
        $this->assertTrue($result['hasgrade']);
1068
        $this->assertEquals(100.0, $result['grade']);
1069
 
1070
        // End the testing for quizapi2 that do not allow the student to view the grade.
1071
 
1072
    }
1073
    /**
1074
     * Test get_combined_review_options.
1075
     * This is a basic test, this is already tested in display_options_testcase.
1076
     */
11 efrain 1077
    public function test_get_combined_review_options(): void {
1 efrain 1078
        global $DB;
1079
 
1080
        // Create a new quiz with attempts.
1081
        $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
1082
        $data = ['course' => $this->course->id,
1083
                      'sumgrades' => 1];
1084
        $quiz = $quizgenerator->create_instance($data);
1085
 
1086
        // Create a couple of questions.
1087
        $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
1088
 
1089
        $cat = $questiongenerator->create_question_category();
1090
        $question = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]);
1091
        quiz_add_quiz_question($question->id, $quiz);
1092
 
1093
        $quizobj = quiz_settings::create($quiz->id, $this->student->id);
1094
 
1095
        // Set grade to pass.
1096
        $item = \grade_item::fetch(['courseid' => $this->course->id, 'itemtype' => 'mod',
1097
                                        'itemmodule' => 'quiz', 'iteminstance' => $quiz->id, 'outcomeid' => null]);
1098
        $item->gradepass = 80;
1099
        $item->update();
1100
 
1101
        // Start the passing attempt.
1102
        $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
1103
        $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
1104
 
1105
        $timenow = time();
1106
        $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $this->student->id);
1107
        quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
1108
        quiz_attempt_save_started($quizobj, $quba, $attempt);
1109
 
1110
        $this->setUser($this->student);
1111
 
1112
        $result = mod_quiz_external::get_combined_review_options($quiz->id);
1113
        $result = external_api::clean_returnvalue(mod_quiz_external::get_combined_review_options_returns(), $result);
1114
 
1115
        // Expected values.
1116
        $expected = [
1117
            "someoptions" => [
1118
                ["name" => "feedback", "value" => 1],
1119
                ["name" => "generalfeedback", "value" => 1],
1120
                ["name" => "rightanswer", "value" => 1],
1121
                ["name" => "overallfeedback", "value" => 0],
1122
                ["name" => "marks", "value" => 2],
1123
            ],
1124
            "alloptions" => [
1125
                ["name" => "feedback", "value" => 1],
1126
                ["name" => "generalfeedback", "value" => 1],
1127
                ["name" => "rightanswer", "value" => 1],
1128
                ["name" => "overallfeedback", "value" => 0],
1129
                ["name" => "marks", "value" => 2],
1130
            ],
1131
            "warnings" => [],
1132
        ];
1133
 
1134
        $this->assertEquals($expected, $result);
1135
 
1136
        // Now, finish the attempt.
1137
        $attemptobj = quiz_attempt::create($attempt->id);
1441 ariadna 1138
        $attemptobj->process_submit($timenow, false);
1139
        $attemptobj->process_grade_submission($timenow);
1 efrain 1140
 
1141
        $expected = [
1142
            "someoptions" => [
1143
                ["name" => "feedback", "value" => 1],
1144
                ["name" => "generalfeedback", "value" => 1],
1145
                ["name" => "rightanswer", "value" => 1],
1146
                ["name" => "overallfeedback", "value" => 1],
1147
                ["name" => "marks", "value" => 2],
1148
            ],
1149
            "alloptions" => [
1150
                ["name" => "feedback", "value" => 1],
1151
                ["name" => "generalfeedback", "value" => 1],
1152
                ["name" => "rightanswer", "value" => 1],
1153
                ["name" => "overallfeedback", "value" => 1],
1154
                ["name" => "marks", "value" => 2],
1155
            ],
1156
            "warnings" => [],
1157
        ];
1158
 
1159
        // We should see now the overall feedback.
1160
        $result = mod_quiz_external::get_combined_review_options($quiz->id);
1161
        $result = external_api::clean_returnvalue(mod_quiz_external::get_combined_review_options_returns(), $result);
1162
        $this->assertEquals($expected, $result);
1163
 
1164
        // Start a new attempt, but not finish it.
1165
        $timenow = time();
1166
        $attempt = quiz_create_attempt($quizobj, 2, false, $timenow, false, $this->student->id);
1167
        $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
1168
        $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
1169
        quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
1170
        quiz_attempt_save_started($quizobj, $quba, $attempt);
1171
 
1172
        $expected = [
1173
            "someoptions" => [
1174
                ["name" => "feedback", "value" => 1],
1175
                ["name" => "generalfeedback", "value" => 1],
1176
                ["name" => "rightanswer", "value" => 1],
1177
                ["name" => "overallfeedback", "value" => 1],
1178
                ["name" => "marks", "value" => 2],
1179
            ],
1180
            "alloptions" => [
1181
                ["name" => "feedback", "value" => 1],
1182
                ["name" => "generalfeedback", "value" => 1],
1183
                ["name" => "rightanswer", "value" => 1],
1184
                ["name" => "overallfeedback", "value" => 0],
1185
                ["name" => "marks", "value" => 2],
1186
            ],
1187
            "warnings" => [],
1188
        ];
1189
 
1190
        $result = mod_quiz_external::get_combined_review_options($quiz->id);
1191
        $result = external_api::clean_returnvalue(mod_quiz_external::get_combined_review_options_returns(), $result);
1192
        $this->assertEquals($expected, $result);
1193
 
1194
        // Teacher, for see student options.
1195
        $this->setUser($this->teacher);
1196
 
1197
        $result = mod_quiz_external::get_combined_review_options($quiz->id, $this->student->id);
1198
        $result = external_api::clean_returnvalue(mod_quiz_external::get_combined_review_options_returns(), $result);
1199
 
1200
        $this->assertEquals($expected, $result);
1201
 
1202
        // Invalid user.
1203
        try {
1204
            mod_quiz_external::get_combined_review_options($quiz->id, -1);
1205
            $this->fail('Exception expected due to missing capability.');
1206
        } catch (\dml_missing_record_exception $e) {
1207
            $this->assertEquals('invaliduser', $e->errorcode);
1208
        }
1209
    }
1210
 
1211
    /**
1212
     * Test get_combined_review_options when the user has an override.
1213
     *
1214
     * @covers ::get_combined_review_options
1215
     * @covers ::get_combined_review_options_parameters
1216
     * @covers ::get_combined_review_options_returns
1217
     */
1218
    public function test_get_combined_review_options_with_overrides(): void {
1219
        global $DB;
1220
 
1221
        // Create a closed quiz with review marks only when quiz is closed.
1222
        list($quiz, $context, $quizobj) = $this->create_quiz_with_questions(true, true, 'deferredfeedback', false, [
1223
            'timeclose' => time() - HOURSECS,
1224
            'marksduring' => 0,
1225
            'maxmarksduring' => 0,
1226
            'marksimmediately' => 0,
1227
            'maxmarksimmediately' => 0,
1228
            'marksopen' => 0,
1229
            'maxmarksopen' => 0,
1230
            'marksclosed' => 1,
1231
            'maxmarksclosed' => 1,
1232
        ]);
1233
 
1234
        // Check that the student can see the marks because the quiz is closed.
1235
        $this->setUser($this->student);
1236
 
1237
        $expected = [
1238
            "someoptions" => [
1239
                ["name" => "feedback", "value" => 1],
1240
                ["name" => "generalfeedback", "value" => 1],
1241
                ["name" => "rightanswer", "value" => 1],
1242
                ["name" => "overallfeedback", "value" => 1],
1243
                ["name" => "marks", "value" => 2],
1244
            ],
1245
            "alloptions" => [
1246
                ["name" => "feedback", "value" => 1],
1247
                ["name" => "generalfeedback", "value" => 1],
1248
                ["name" => "rightanswer", "value" => 1],
1249
                ["name" => "overallfeedback", "value" => 1],
1250
                ["name" => "marks", "value" => 2],
1251
            ],
1252
            "warnings" => [],
1253
        ];
1254
 
1255
        $result = mod_quiz_external::get_combined_review_options($quiz->id);
1256
        $result = external_api::clean_returnvalue(mod_quiz_external::get_combined_review_options_returns(), $result);
1257
 
1258
        $this->assertEquals($expected, $result);
1259
 
1260
        // Add an override for the student to increase the close time.
1261
        $DB->insert_record('quiz_overrides', [
1262
            'quiz' => $quiz->id,
1263
            'userid' => $this->student->id,
1264
            'timeclose' => time() + HOURSECS,
1265
        ]);
1266
 
1267
        // Check that now the marks option has changed.
1268
        $expected = [
1269
            "someoptions" => [
1270
                ["name" => "feedback", "value" => 1],
1271
                ["name" => "generalfeedback", "value" => 1],
1272
                ["name" => "rightanswer", "value" => 1],
1273
                ["name" => "overallfeedback", "value" => 1],
1274
                ["name" => "marks", "value" => 0],
1275
            ],
1276
            "alloptions" => [
1277
                ["name" => "feedback", "value" => 1],
1278
                ["name" => "generalfeedback", "value" => 1],
1279
                ["name" => "rightanswer", "value" => 1],
1280
                ["name" => "overallfeedback", "value" => 1],
1281
                ["name" => "marks", "value" => 0],
1282
            ],
1283
            "warnings" => [],
1284
        ];
1285
 
1286
        $result = mod_quiz_external::get_combined_review_options($quiz->id);
1287
        $result = external_api::clean_returnvalue(mod_quiz_external::get_combined_review_options_returns(), $result);
1288
 
1289
        $this->assertEquals($expected, $result);
1290
    }
1291
 
1292
    /**
1293
     * Test start_attempt
1294
     */
11 efrain 1295
    public function test_start_attempt(): void {
1 efrain 1296
        global $DB;
1297
 
1298
        // Create a new quiz with questions.
1299
        list($quiz, $context, $quizobj) = $this->create_quiz_with_questions();
1300
 
1301
        $this->setUser($this->student);
1302
 
1303
        // Try to open attempt in closed quiz.
1304
        $quiz->timeopen = time() - WEEKSECS;
1305
        $quiz->timeclose = time() - DAYSECS;
1306
        $DB->update_record('quiz', $quiz);
1307
        $result = mod_quiz_external::start_attempt($quiz->id);
1308
        $result = external_api::clean_returnvalue(mod_quiz_external::start_attempt_returns(), $result);
1309
 
1310
        $this->assertEquals([], $result['attempt']);
1311
        $this->assertCount(1, $result['warnings']);
1312
 
1313
        // Now with a password.
1314
        $quiz->timeopen = 0;
1315
        $quiz->timeclose = 0;
1316
        $quiz->password = 'abc';
1317
        $DB->update_record('quiz', $quiz);
1318
 
1319
        try {
1320
            mod_quiz_external::start_attempt($quiz->id, [["name" => "quizpassword", "value" => 'bad']]);
1321
            $this->fail('Exception expected due to invalid passwod.');
1322
        } catch (moodle_exception $e) {
1323
            $this->assertEquals(get_string('passworderror', 'quizaccess_password'), $e->errorcode);
1324
        }
1325
 
1326
        // Now, try everything correct.
1327
        $result = mod_quiz_external::start_attempt($quiz->id, [["name" => "quizpassword", "value" => 'abc']]);
1328
        $result = external_api::clean_returnvalue(mod_quiz_external::start_attempt_returns(), $result);
1329
 
1330
        $this->assertEquals(1, $result['attempt']['attempt']);
1331
        $this->assertEquals($this->student->id, $result['attempt']['userid']);
1332
        $this->assertEquals($quiz->id, $result['attempt']['quiz']);
1333
        $this->assertCount(0, $result['warnings']);
1334
        $attemptid = $result['attempt']['id'];
1335
 
1336
        // We are good, try to start a new attempt now.
1337
 
1338
        try {
1339
            mod_quiz_external::start_attempt($quiz->id, [["name" => "quizpassword", "value" => 'abc']]);
1340
            $this->fail('Exception expected due to attempt not finished.');
1341
        } catch (moodle_exception $e) {
1342
            $this->assertEquals('attemptstillinprogress', $e->errorcode);
1343
        }
1344
 
1345
        // Finish the started attempt.
1346
 
1347
        // Process some responses from the student.
1348
        $timenow = time();
1349
        $attemptobj = quiz_attempt::create($attemptid);
1350
        $tosubmit = [1 => ['answer' => '3.14']];
1351
        $attemptobj->process_submitted_actions($timenow, false, $tosubmit);
1352
 
1353
        // Finish the attempt.
1354
        $attemptobj = quiz_attempt::create($attemptid);
1355
        $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
1441 ariadna 1356
        $attemptobj->process_submit($timenow, false);
1357
        $attemptobj->process_grade_submission($timenow);
1 efrain 1358
 
1359
        // We should be able to start a new attempt.
1360
        $result = mod_quiz_external::start_attempt($quiz->id, [["name" => "quizpassword", "value" => 'abc']]);
1361
        $result = external_api::clean_returnvalue(mod_quiz_external::start_attempt_returns(), $result);
1362
 
1363
        $this->assertEquals(2, $result['attempt']['attempt']);
1364
        $this->assertEquals($this->student->id, $result['attempt']['userid']);
1365
        $this->assertEquals($quiz->id, $result['attempt']['quiz']);
1366
        $this->assertCount(0, $result['warnings']);
1367
 
1368
        // Test user with no capabilities.
1369
        // We need a explicit prohibit since this capability is only defined in authenticated user and guest roles.
1370
        assign_capability('mod/quiz:attempt', CAP_PROHIBIT, $this->studentrole->id, $context->id);
1371
        // Empty all the caches that may be affected  by this change.
1372
        accesslib_clear_all_caches_for_unit_testing();
1373
        \course_modinfo::clear_instance_cache();
1374
 
1375
        try {
1376
            mod_quiz_external::start_attempt($quiz->id);
1377
            $this->fail('Exception expected due to missing capability.');
1378
        } catch (\required_capability_exception $e) {
1379
            $this->assertEquals('nopermissions', $e->errorcode);
1380
        }
1381
 
1382
    }
1383
 
1384
    /**
1385
     * Test validate_attempt
1386
     */
11 efrain 1387
    public function test_validate_attempt(): void {
1 efrain 1388
        global $DB;
1389
 
1390
        // Create a new quiz with one attempt started.
1391
        list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true);
1392
 
1393
        $this->setUser($this->student);
1394
 
1395
        // Invalid attempt.
1396
        try {
1397
            $params = ['attemptid' => -1, 'page' => 0];
1398
            testable_mod_quiz_external::validate_attempt($params);
1399
            $this->fail('Exception expected due to invalid attempt id.');
1400
        } catch (\dml_missing_record_exception $e) {
1401
            $this->assertEquals('invalidrecord', $e->errorcode);
1402
        }
1403
 
1404
        // Test OK case.
1405
        $params = ['attemptid' => $attempt->id, 'page' => 0];
1406
        $result = testable_mod_quiz_external::validate_attempt($params);
1407
        $this->assertEquals($attempt->id, $result[0]->get_attempt()->id);
1408
        $this->assertEquals([], $result[1]);
1409
 
1410
        // Test with preflight data.
1411
        $quiz->password = 'abc';
1412
        $DB->update_record('quiz', $quiz);
1413
 
1414
        try {
1415
            $params = ['attemptid' => $attempt->id, 'page' => 0,
1416
                            'preflightdata' => [["name" => "quizpassword", "value" => 'bad']]];
1417
            testable_mod_quiz_external::validate_attempt($params);
1418
            $this->fail('Exception expected due to invalid passwod.');
1419
        } catch (moodle_exception $e) {
1420
            $this->assertEquals(get_string('passworderror', 'quizaccess_password'), $e->errorcode);
1421
        }
1422
 
1423
        // Now, try everything correct.
1424
        $params['preflightdata'][0]['value'] = 'abc';
1425
        $result = testable_mod_quiz_external::validate_attempt($params);
1426
        $this->assertEquals($attempt->id, $result[0]->get_attempt()->id);
1427
        $this->assertEquals([], $result[1]);
1428
 
1429
        // Page out of range.
1430
        $DB->update_record('quiz', $quiz);
1431
        $params['page'] = 4;
1432
        try {
1433
            testable_mod_quiz_external::validate_attempt($params);
1434
            $this->fail('Exception expected due to page out of range.');
1435
        } catch (moodle_exception $e) {
1436
            $this->assertEquals('Invalid page number', $e->errorcode);
1437
        }
1438
 
1439
        $params['page'] = 0;
1440
        // Try to open attempt in closed quiz.
1441
        $quiz->timeopen = time() - WEEKSECS;
1442
        $quiz->timeclose = time() - DAYSECS;
1443
        $DB->update_record('quiz', $quiz);
1444
 
1445
        // This should work, ommit access rules.
1446
        testable_mod_quiz_external::validate_attempt($params, false);
1447
 
1448
        // Get a generic error because prior to checking the dates the attempt is closed.
1449
        try {
1450
            testable_mod_quiz_external::validate_attempt($params);
1451
            $this->fail('Exception expected due to passed dates.');
1452
        } catch (moodle_exception $e) {
1453
            $this->assertEquals('attempterror', $e->errorcode);
1454
        }
1455
 
1456
        // Finish the attempt.
1457
        $attemptobj = quiz_attempt::create($attempt->id);
1441 ariadna 1458
        $attemptobj->process_submit(time(), false);
1459
        $attemptobj->process_grade_submission(time());
1 efrain 1460
 
1461
        try {
1462
            testable_mod_quiz_external::validate_attempt($params, false);
1463
            $this->fail('Exception expected due to attempt finished.');
1464
        } catch (moodle_exception $e) {
1465
            $this->assertEquals('attemptalreadyclosed', $e->errorcode);
1466
        }
1467
 
1468
        // Test user with no capabilities.
1469
        // We need a explicit prohibit since this capability is only defined in authenticated user and guest roles.
1470
        assign_capability('mod/quiz:attempt', CAP_PROHIBIT, $this->studentrole->id, $context->id);
1471
        // Empty all the caches that may be affected  by this change.
1472
        accesslib_clear_all_caches_for_unit_testing();
1473
        \course_modinfo::clear_instance_cache();
1474
 
1475
        try {
1476
            testable_mod_quiz_external::validate_attempt($params);
1477
            $this->fail('Exception expected due to missing permissions.');
1478
        } catch (\required_capability_exception $e) {
1479
            $this->assertEquals('nopermissions', $e->errorcode);
1480
        }
1481
 
1482
        // Now try with a different user.
1483
        $this->setUser($this->teacher);
1484
 
1485
        $params['page'] = 0;
1486
        try {
1487
            testable_mod_quiz_external::validate_attempt($params);
1488
            $this->fail('Exception expected due to not your attempt.');
1489
        } catch (moodle_exception $e) {
1490
            $this->assertEquals('notyourattempt', $e->errorcode);
1491
        }
1492
    }
1493
 
1494
    /**
1495
     * Test get_attempt_data
1496
     */
11 efrain 1497
    public function test_get_attempt_data(): void {
1 efrain 1498
        global $DB;
1499
 
1500
        $timenow = time();
1501
        // Create a new quiz with one attempt started.
1502
        [$quiz, , $quizobj, $attempt] = $this->create_quiz_with_questions(true);
1503
        /** @var structure $structure */
1504
        $structure = $quizobj->get_structure();
1505
        $structure->update_slot_display_number($structure->get_slot_id_for_slot(1), '1.a');
1506
 
1507
        // Set correctness mask so questions state can be fetched only after finishing the attempt.
1508
        $DB->set_field('quiz', 'reviewcorrectness', display_options::IMMEDIATELY_AFTER, ['id' => $quiz->id]);
1509
 
1510
        // Having changed some settings, recreate the objects.
1511
        $attemptobj = quiz_attempt::create($attempt->id);
1512
        $quizobj = $attemptobj->get_quizobj();
1513
        $quizobj->preload_questions();
1514
        $quizobj->load_questions();
1515
        $questions = $quizobj->get_questions();
1516
 
1517
        $this->setUser($this->student);
1518
 
1519
        // We receive one question per page.
1520
        $result = mod_quiz_external::get_attempt_data($attempt->id, 0);
1521
        $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_data_returns(), $result);
1522
 
1523
        $this->assertEquals($attempt, (object) $result['attempt']);
1524
        $this->assertEquals(1, $result['nextpage']);
1525
        $this->assertCount(0, $result['messages']);
1526
        $this->assertCount(1, $result['questions']);
1527
        $this->assertEquals(1, $result['questions'][0]['slot']);
1528
        $this->assertArrayNotHasKey('number', $result['questions'][0]);
1529
        $this->assertEquals('1.a', $result['questions'][0]['questionnumber']);
1530
        $this->assertEquals('numerical', $result['questions'][0]['type']);
1531
        $this->assertEquals('notyetanswered', $result['questions'][0]['stateclass']);
1532
        $this->assertArrayNotHasKey('state', $result['questions'][0]);  // We don't receive the state yet.
1533
        $this->assertEquals('notyetanswered', $result['questions'][0]['stateclass']);
1534
        $this->assertEquals(get_string('notyetanswered', 'question'), $result['questions'][0]['status']);
1535
        $this->assertFalse($result['questions'][0]['flagged']);
1536
        $this->assertEquals(0, $result['questions'][0]['page']);
1537
        $this->assertEmpty($result['questions'][0]['mark']);
1538
        $this->assertEquals(1, $result['questions'][0]['maxmark']);
1539
        $this->assertEquals(1, $result['questions'][0]['sequencecheck']);
1441 ariadna 1540
        $this->assertEquals(\question_attempt_step::TIMECREATED_ON_FIRST_RENDER, $result['questions'][0]['lastactiontime']);
1 efrain 1541
        $this->assertEquals(false, $result['questions'][0]['hasautosavedstep']);
1542
 
1543
        // Now try the last page.
1544
        $result = mod_quiz_external::get_attempt_data($attempt->id, 1);
1545
        $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_data_returns(), $result);
1546
 
1547
        $this->assertEquals($attempt, (object) $result['attempt']);
1548
        $this->assertEquals(-1, $result['nextpage']);
1549
        $this->assertCount(0, $result['messages']);
1550
        $this->assertCount(1, $result['questions']);
1551
        $this->assertEquals(2, $result['questions'][0]['slot']);
1552
        $this->assertEquals(2, $result['questions'][0]['questionnumber']);
1553
        $this->assertEquals(2, $result['questions'][0]['number']);
1554
        $this->assertEquals('numerical', $result['questions'][0]['type']);
1555
        $this->assertEquals('notyetanswered', $result['questions'][0]['stateclass']);
1556
        $this->assertArrayNotHasKey('state', $result['questions'][0]);  // We don't receive the state yet.
1557
        $this->assertEquals(get_string('notyetanswered', 'question'), $result['questions'][0]['status']);
1558
        $this->assertFalse($result['questions'][0]['flagged']);
1559
        $this->assertEquals(1, $result['questions'][0]['page']);
1560
        $this->assertEquals(1, $result['questions'][0]['sequencecheck']);
1441 ariadna 1561
        $this->assertEquals(\question_attempt_step::TIMECREATED_ON_FIRST_RENDER, $result['questions'][0]['lastactiontime']);
1 efrain 1562
        $this->assertEquals(false, $result['questions'][0]['hasautosavedstep']);
1563
 
1564
        // Finish previous attempt.
1441 ariadna 1565
        $attemptobj->process_submit(time(), false);
1566
        $attemptobj->process_grade_submission(time());
1 efrain 1567
 
1568
        // Now we should receive the question state.
1569
        $result = mod_quiz_external::get_attempt_review($attempt->id, 1);
1570
        $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_review_returns(), $result);
1571
        $this->assertEquals('notanswered', $result['questions'][0]['stateclass']);
1572
        $this->assertEquals('gaveup', $result['questions'][0]['state']);
1573
 
1574
        // Change setting and expect two pages.
1575
        $quiz->questionsperpage = 4;
1576
        $DB->update_record('quiz', $quiz);
1577
        quiz_repaginate_questions($quiz->id, $quiz->questionsperpage);
1578
 
1579
        // Start with new attempt with the new layout.
1580
        $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
1581
        $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
1582
 
1583
        $timenow = time();
1584
        $attempt = quiz_create_attempt($quizobj, 2, false, $timenow, false, $this->student->id);
1585
        quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
1586
        quiz_attempt_save_started($quizobj, $quba, $attempt);
1587
 
1588
        // We receive two questions per page.
1589
        $result = mod_quiz_external::get_attempt_data($attempt->id, 0);
1590
        $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_data_returns(), $result);
1591
        $this->assertCount(2, $result['questions']);
1592
        $this->assertEquals(-1, $result['nextpage']);
1593
 
1594
        // Check questions looks good.
1595
        $found = 0;
1596
        foreach ($questions as $question) {
1597
            foreach ($result['questions'] as $rquestion) {
1598
                if ($rquestion['slot'] == $question->slot) {
1599
                    $this->assertTrue(strpos($rquestion['html'], "qid=$question->id") !== false);
1600
                    $found++;
1601
                }
1602
            }
1603
        }
1604
        $this->assertEquals(2, $found);
1605
 
1606
    }
1607
 
1608
    /**
1609
     * Test get_attempt_data with blocked questions.
1610
     * @since 3.2
1611
     */
11 efrain 1612
    public function test_get_attempt_data_with_blocked_questions(): void {
1 efrain 1613
        global $DB;
1614
 
1615
        // Create a new quiz with one attempt started and using immediatefeedback.
1616
        list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(
1617
                true, false, 'immediatefeedback');
1618
 
1619
        $quizobj = $attemptobj->get_quizobj();
1620
 
1621
        // Make second question blocked by the first one.
1622
        $structure = $quizobj->get_structure();
1623
        $slots = $structure->get_slots();
1624
        $structure->update_question_dependency(end($slots)->id, true);
1625
 
1626
        $quizobj->preload_questions();
1627
        $quizobj->load_questions();
1628
        $questions = $quizobj->get_questions();
1629
 
1630
        $this->setUser($this->student);
1631
 
1632
        // We receive one question per page.
1633
        $result = mod_quiz_external::get_attempt_data($attempt->id, 0);
1634
        $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_data_returns(), $result);
1635
 
1636
        $this->assertEquals($attempt, (object) $result['attempt']);
1637
        $this->assertCount(1, $result['questions']);
1638
        $this->assertEquals(1, $result['questions'][0]['slot']);
1639
        $this->assertEquals(1, $result['questions'][0]['number']);
1640
        $this->assertEquals(false, $result['questions'][0]['blockedbyprevious']);
1641
 
1642
        // Now try the last page.
1643
        $result = mod_quiz_external::get_attempt_data($attempt->id, 1);
1644
        $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_data_returns(), $result);
1645
 
1646
        $this->assertEquals($attempt, (object) $result['attempt']);
1647
        $this->assertCount(1, $result['questions']);
1648
        $this->assertEquals(2, $result['questions'][0]['slot']);
1649
        $this->assertEquals(2, $result['questions'][0]['number']);
1650
        $this->assertEquals(true, $result['questions'][0]['blockedbyprevious']);
1651
    }
1652
 
1653
    /**
1654
     * Test get_attempt_summary
1655
     */
11 efrain 1656
    public function test_get_attempt_summary(): void {
1 efrain 1657
 
1658
        $timenow = time();
1659
        // Create a new quiz with one attempt started.
1660
        list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true);
1661
 
1662
        $this->setUser($this->student);
1663
        $result = mod_quiz_external::get_attempt_summary($attempt->id);
1664
        $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result);
1665
 
1666
        // Check the state, flagged and mark data is correct.
1667
        $this->assertEquals('todo', $result['questions'][0]['state']);
1668
        $this->assertEquals('notyetanswered', $result['questions'][0]['stateclass']);
1669
        $this->assertEquals('todo', $result['questions'][1]['state']);
1670
        $this->assertEquals('notyetanswered', $result['questions'][1]['stateclass']);
1671
        $this->assertEquals(1, $result['questions'][0]['number']);
1672
        $this->assertEquals(2, $result['questions'][1]['number']);
1673
        $this->assertFalse($result['questions'][0]['flagged']);
1674
        $this->assertFalse($result['questions'][1]['flagged']);
1675
        $this->assertEmpty($result['questions'][0]['mark']);
1676
        $this->assertEmpty($result['questions'][1]['mark']);
1677
        $this->assertEquals(1, $result['questions'][0]['sequencecheck']);
1678
        $this->assertEquals(1, $result['questions'][1]['sequencecheck']);
1441 ariadna 1679
        $this->assertEquals(\question_attempt_step::TIMECREATED_ON_FIRST_RENDER, $result['questions'][0]['lastactiontime']);
1680
        $this->assertEquals(\question_attempt_step::TIMECREATED_ON_FIRST_RENDER, $result['questions'][1]['lastactiontime']);
1 efrain 1681
        $this->assertEquals(false, $result['questions'][0]['hasautosavedstep']);
1682
        $this->assertEquals(false, $result['questions'][1]['hasautosavedstep']);
1683
 
1684
        // Check question options.
1685
        $this->assertNotEmpty(5, $result['questions'][0]['settings']);
1686
        // Check at least some settings returned.
1687
        $this->assertCount(4, (array) json_decode($result['questions'][0]['settings']));
1688
        $this->assertEquals(2, $result['totalunanswered']); // All questions are unanswered.
1689
 
1690
        // Submit a response for the first question.
1691
        $tosubmit = [1 => ['answer' => '3.14']];
1692
        $attemptobj->process_submitted_actions(time(), false, $tosubmit);
1693
        $result = mod_quiz_external::get_attempt_summary($attempt->id);
1694
        $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result);
1695
 
1696
        // Check it's marked as completed only the first one.
1697
        $this->assertEquals('complete', $result['questions'][0]['state']);
1698
        $this->assertEquals('answersaved', $result['questions'][0]['stateclass']);
1699
        $this->assertEquals('todo', $result['questions'][1]['state']);
1700
        $this->assertEquals('notyetanswered', $result['questions'][1]['stateclass']);
1701
        $this->assertEquals(1, $result['questions'][0]['number']);
1702
        $this->assertEquals(2, $result['questions'][1]['number']);
1703
        $this->assertFalse($result['questions'][0]['flagged']);
1704
        $this->assertFalse($result['questions'][1]['flagged']);
1705
        $this->assertEmpty($result['questions'][0]['mark']);
1706
        $this->assertEmpty($result['questions'][1]['mark']);
1707
        $this->assertEquals(2, $result['questions'][0]['sequencecheck']);
1708
        $this->assertEquals(1, $result['questions'][1]['sequencecheck']);
1709
        $this->assertGreaterThanOrEqual($timenow, $result['questions'][0]['lastactiontime']);
1710
        $this->assertGreaterThanOrEqual($timenow, $result['questions'][1]['lastactiontime']);
1711
        $this->assertEquals(false, $result['questions'][0]['hasautosavedstep']);
1712
        $this->assertEquals(false, $result['questions'][1]['hasautosavedstep']);
1713
        $this->assertEquals(1, $result['totalunanswered']); // Only one question is unanswered.
1714
    }
1715
 
1716
    /**
1717
     * Test save_attempt
1718
     */
11 efrain 1719
    public function test_save_attempt(): void {
1 efrain 1720
 
1721
        $timenow = time();
1722
        // Create a new quiz with one attempt started.
1723
        list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true);
1724
 
1725
        // Response for slot 1.
1726
        $prefix = $quba->get_field_prefix(1);
1727
        $data = [
1728
            ['name' => 'slots', 'value' => 1],
1729
            ['name' => $prefix . ':sequencecheck',
1730
                    'value' => $attemptobj->get_question_attempt(1)->get_sequence_check_count()],
1731
            ['name' => $prefix . 'answer', 'value' => 1],
1732
        ];
1733
 
1734
        $this->setUser($this->student);
1735
 
1736
        $result = mod_quiz_external::save_attempt($attempt->id, $data);
1737
        $result = external_api::clean_returnvalue(mod_quiz_external::save_attempt_returns(), $result);
1738
        $this->assertTrue($result['status']);
1739
 
1740
        // Now, get the summary.
1741
        $result = mod_quiz_external::get_attempt_summary($attempt->id);
1742
        $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result);
1743
 
1744
        // Check it's marked as completed only the first one.
1745
        $this->assertEquals('complete', $result['questions'][0]['state']);
1746
        $this->assertEquals('answersaved', $result['questions'][0]['stateclass']);
1747
        $this->assertEquals('todo', $result['questions'][1]['state']);
1748
        $this->assertEquals('notyetanswered', $result['questions'][1]['stateclass']);
1749
        $this->assertEquals(1, $result['questions'][0]['number']);
1750
        $this->assertEquals(2, $result['questions'][1]['number']);
1751
        $this->assertFalse($result['questions'][0]['flagged']);
1752
        $this->assertFalse($result['questions'][1]['flagged']);
1753
        $this->assertEmpty($result['questions'][0]['mark']);
1754
        $this->assertEmpty($result['questions'][1]['mark']);
1755
        $this->assertEquals(1, $result['questions'][0]['sequencecheck']);
1756
        $this->assertEquals(1, $result['questions'][1]['sequencecheck']);
1757
        $this->assertGreaterThanOrEqual($timenow, $result['questions'][0]['lastactiontime']);
1441 ariadna 1758
        $this->assertEquals(\question_attempt_step::TIMECREATED_ON_FIRST_RENDER, $result['questions'][1]['lastactiontime']);
1 efrain 1759
        $this->assertEquals(true, $result['questions'][0]['hasautosavedstep']);
1760
        $this->assertEquals(false, $result['questions'][1]['hasautosavedstep']);
1761
 
1762
        // Now, second slot.
1763
        $prefix = $quba->get_field_prefix(2);
1764
        $data = [
1765
            ['name' => 'slots', 'value' => 2],
1766
            ['name' => $prefix . ':sequencecheck',
1767
                    'value' => $attemptobj->get_question_attempt(1)->get_sequence_check_count()],
1768
            ['name' => $prefix . 'answer', 'value' => 1],
1769
        ];
1770
 
1771
        $result = mod_quiz_external::save_attempt($attempt->id, $data);
1772
        $result = external_api::clean_returnvalue(mod_quiz_external::save_attempt_returns(), $result);
1773
        $this->assertTrue($result['status']);
1774
 
1775
        // Now, get the summary.
1776
        $result = mod_quiz_external::get_attempt_summary($attempt->id);
1777
        $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result);
1778
 
1779
        // Check it's marked as completed only the first one.
1780
        $this->assertEquals('complete', $result['questions'][0]['state']);
1781
        $this->assertEquals('answersaved', $result['questions'][0]['stateclass']);
1782
        $this->assertEquals(1, $result['questions'][0]['sequencecheck']);
1783
        $this->assertEquals('complete', $result['questions'][1]['state']);
1784
        $this->assertEquals('answersaved', $result['questions'][1]['stateclass']);
1785
        $this->assertEquals(1, $result['questions'][1]['sequencecheck']);
1786
 
1787
    }
1788
 
1789
    /**
1790
     * Test process_attempt
1791
     */
11 efrain 1792
    public function test_process_attempt(): void {
1 efrain 1793
        global $DB;
1794
 
1795
        $timenow = time();
1796
        // Create a new quiz with three questions and one attempt started.
1797
        list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true, false,
1798
            'deferredfeedback', true);
1799
 
1800
        // Response for slot 1.
1801
        $prefix = $quba->get_field_prefix(1);
1802
        $data = [
1803
            ['name' => 'slots', 'value' => 1],
1804
            ['name' => $prefix . ':sequencecheck',
1805
                    'value' => $attemptobj->get_question_attempt(1)->get_sequence_check_count()],
1806
            ['name' => $prefix . 'answer', 'value' => 1],
1807
        ];
1808
 
1809
        $this->setUser($this->student);
1810
 
1811
        $result = mod_quiz_external::process_attempt($attempt->id, $data);
1812
        $result = external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result);
1813
        $this->assertEquals(quiz_attempt::IN_PROGRESS, $result['state']);
1814
 
1815
        $result = mod_quiz_external::get_attempt_data($attempt->id, 2);
1816
 
1817
        // Now, get the summary.
1818
        $result = mod_quiz_external::get_attempt_summary($attempt->id);
1819
        $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result);
1820
 
1821
        // Check it's marked as completed only the first one.
1822
        $this->assertEquals('complete', $result['questions'][0]['state']);
1823
        $this->assertEquals('todo', $result['questions'][1]['state']);
1824
        $this->assertEquals(1, $result['questions'][0]['number']);
1825
        $this->assertEquals(2, $result['questions'][1]['number']);
1826
        $this->assertFalse($result['questions'][0]['flagged']);
1827
        $this->assertFalse($result['questions'][1]['flagged']);
1828
        $this->assertEmpty($result['questions'][0]['mark']);
1829
        $this->assertEmpty($result['questions'][1]['mark']);
1830
        $this->assertEquals(2, $result['questions'][0]['sequencecheck']);
1831
        $this->assertEquals(2, $result['questions'][0]['sequencecheck']);
1832
        $this->assertGreaterThanOrEqual($timenow, $result['questions'][0]['lastactiontime']);
1833
        $this->assertGreaterThanOrEqual($timenow, $result['questions'][0]['lastactiontime']);
1834
        $this->assertEquals(false, $result['questions'][0]['hasautosavedstep']);
1835
        $this->assertEquals(false, $result['questions'][0]['hasautosavedstep']);
1836
 
1837
        // Now, second slot.
1838
        $prefix = $quba->get_field_prefix(2);
1839
        $data = [
1840
            ['name' => 'slots', 'value' => 2],
1841
            ['name' => $prefix . ':sequencecheck',
1842
                    'value' => $attemptobj->get_question_attempt(1)->get_sequence_check_count()],
1843
            ['name' => $prefix . 'answer', 'value' => 1],
1844
            ['name' => $prefix . ':flagged', 'value' => 1],
1845
        ];
1846
 
1847
        $result = mod_quiz_external::process_attempt($attempt->id, $data);
1848
        $result = external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result);
1849
        $this->assertEquals(quiz_attempt::IN_PROGRESS, $result['state']);
1850
 
1851
        // Now, get the summary.
1852
        $result = mod_quiz_external::get_attempt_summary($attempt->id);
1853
        $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result);
1854
 
1855
        // Check it's marked as completed the two first questions.
1856
        $this->assertEquals('complete', $result['questions'][0]['state']);
1857
        $this->assertEquals('complete', $result['questions'][1]['state']);
1858
        $this->assertFalse($result['questions'][0]['flagged']);
1859
        $this->assertTrue($result['questions'][1]['flagged']);
1860
 
1861
        // Add files in the attachment response.
1862
        $draftitemid = file_get_unused_draft_itemid();
1863
        $filerecordinline = [
1864
            'contextid' => \context_user::instance($this->student->id)->id,
1865
            'component' => 'user',
1866
            'filearea'  => 'draft',
1867
            'itemid'    => $draftitemid,
1868
            'filepath'  => '/',
1869
            'filename'  => 'faketxt.txt',
1870
        ];
1871
        $fs = get_file_storage();
1872
        $fs->create_file_from_string($filerecordinline, 'fake txt contents 1.');
1873
 
1874
        // Last slot.
1875
        $prefix = $quba->get_field_prefix(3);
1876
        $data = [
1877
            ['name' => 'slots', 'value' => 3],
1878
            ['name' => $prefix . ':sequencecheck',
1879
                    'value' => $attemptobj->get_question_attempt(1)->get_sequence_check_count()],
1880
            ['name' => $prefix . 'answer', 'value' => 'Some test'],
1881
            ['name' => $prefix . 'answerformat', 'value' => FORMAT_HTML],
1882
            ['name' => $prefix . 'attachments', 'value' => $draftitemid],
1883
        ];
1884
 
1885
        $result = mod_quiz_external::process_attempt($attempt->id, $data);
1886
        $result = external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result);
1887
        $this->assertEquals(quiz_attempt::IN_PROGRESS, $result['state']);
1888
 
1889
        // Now, get the summary.
1890
        $result = mod_quiz_external::get_attempt_summary($attempt->id);
1891
        $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result);
1892
 
1893
        $this->assertEquals('complete', $result['questions'][0]['state']);
1894
        $this->assertEquals('complete', $result['questions'][1]['state']);
1895
        $this->assertEquals('complete', $result['questions'][2]['state']);
1896
        $this->assertFalse($result['questions'][0]['flagged']);
1897
        $this->assertTrue($result['questions'][1]['flagged']);
1898
        $this->assertFalse($result['questions'][2]['flagged']);
1899
 
1900
        // Check submitted files are there.
1901
        $this->assertCount(1, $result['questions'][2]['responsefileareas']);
1902
        $this->assertEquals('attachments', $result['questions'][2]['responsefileareas'][0]['area']);
1903
        $this->assertCount(1, $result['questions'][2]['responsefileareas'][0]['files']);
1904
        $this->assertEquals($filerecordinline['filename'], $result['questions'][2]['responsefileareas'][0]['files'][0]['filename']);
1905
 
1906
        // Finish the attempt.
1907
        $sink = $this->redirectMessages();
1908
        $result = mod_quiz_external::process_attempt($attempt->id, [], true);
1909
        $result = external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result);
1910
        $this->assertEquals(quiz_attempt::FINISHED, $result['state']);
1911
        $messages = $sink->get_messages();
1912
        $message = reset($messages);
1913
        $sink->close();
1914
        // Test customdata.
1915
        if (!empty($message->customdata)) {
1916
            $customdata = json_decode($message->customdata);
1917
            $this->assertEquals($quizobj->get_quizid(), $customdata->instance);
1918
            $this->assertEquals($quizobj->get_cmid(), $customdata->cmid);
1919
            $this->assertEquals($attempt->id, $customdata->attemptid);
1920
            $this->assertObjectHasProperty('notificationiconurl', $customdata);
1921
        }
1922
 
1923
        // Start new attempt.
1924
        $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
1925
        $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
1926
 
1927
        $timenow = time();
1928
        $attempt = quiz_create_attempt($quizobj, 2, false, $timenow, false, $this->student->id);
1929
        quiz_start_new_attempt($quizobj, $quba, $attempt, 2, $timenow);
1930
        quiz_attempt_save_started($quizobj, $quba, $attempt);
1931
 
1932
        // Force grace period, attempt going to overdue.
1933
        $quiz->timeclose = $timenow - 10;
1934
        $quiz->graceperiod = 60;
1935
        $quiz->overduehandling = 'graceperiod';
1936
        $DB->update_record('quiz', $quiz);
1937
 
1938
        $result = mod_quiz_external::process_attempt($attempt->id, []);
1939
        $result = external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result);
1940
        $this->assertEquals(quiz_attempt::OVERDUE, $result['state']);
1941
 
1942
        // Force grace period for time limit.
1943
        $quiz->timeclose = 0;
1944
        $quiz->timelimit = 1;
1945
        $quiz->graceperiod = 60;
1946
        $quiz->overduehandling = 'graceperiod';
1947
        $DB->update_record('quiz', $quiz);
1948
 
1949
        $timenow = time();
1950
        $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
1951
        $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
1952
        $attempt = quiz_create_attempt($quizobj, 3, 2, $timenow - 10, false, $this->student->id);
1953
        quiz_start_new_attempt($quizobj, $quba, $attempt, 2, $timenow - 10);
1954
        quiz_attempt_save_started($quizobj, $quba, $attempt);
1955
 
1956
        $result = mod_quiz_external::process_attempt($attempt->id, []);
1957
        $result = external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result);
1958
        $this->assertEquals(quiz_attempt::OVERDUE, $result['state']);
1959
 
1960
        // New attempt.
1961
        $timenow = time();
1962
        $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
1963
        $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
1964
        $attempt = quiz_create_attempt($quizobj, 4, 3, $timenow, false, $this->student->id);
1965
        quiz_start_new_attempt($quizobj, $quba, $attempt, 3, $timenow);
1966
        quiz_attempt_save_started($quizobj, $quba, $attempt);
1967
 
1968
        // Force abandon.
1969
        $quiz->timeclose = $timenow - HOURSECS;
1970
        $DB->update_record('quiz', $quiz);
1971
 
1972
        $result = mod_quiz_external::process_attempt($attempt->id, []);
1973
        $result = external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result);
1974
        $this->assertEquals(quiz_attempt::ABANDONED, $result['state']);
1975
 
1976
    }
1977
 
1978
    /**
1979
     * Test validate_attempt_review
1980
     */
11 efrain 1981
    public function test_validate_attempt_review(): void {
1 efrain 1982
        global $DB;
1983
 
1984
        // Create a new quiz with one attempt started.
1985
        list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true);
1986
 
1987
        $this->setUser($this->student);
1988
 
1989
        // Invalid attempt, invalid id.
1990
        try {
1991
            $params = ['attemptid' => -1];
1992
            testable_mod_quiz_external::validate_attempt_review($params);
1993
            $this->fail('Exception expected due invalid id.');
1994
        } catch (\dml_missing_record_exception $e) {
1995
            $this->assertEquals('invalidrecord', $e->errorcode);
1996
        }
1997
 
1998
        // Invalid attempt, not closed.
1999
        try {
2000
            $params = ['attemptid' => $attempt->id];
2001
            testable_mod_quiz_external::validate_attempt_review($params);
2002
            $this->fail('Exception expected due not closed attempt.');
2003
        } catch (moodle_exception $e) {
2004
            $this->assertEquals('attemptclosed', $e->errorcode);
2005
        }
2006
 
2007
        // Test ok case (finished attempt).
2008
        list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true, true);
2009
 
2010
        $params = ['attemptid' => $attempt->id];
2011
        testable_mod_quiz_external::validate_attempt_review($params);
2012
 
2013
        // Teacher should be able to view the review of one student's attempt.
2014
        $this->setUser($this->teacher);
2015
        testable_mod_quiz_external::validate_attempt_review($params);
2016
 
2017
        // We should not see other students attempts.
2018
        $anotherstudent = self::getDataGenerator()->create_user();
2019
        $this->getDataGenerator()->enrol_user($anotherstudent->id, $this->course->id, $this->studentrole->id, 'manual');
2020
 
2021
        $this->setUser($anotherstudent);
2022
        try {
2023
            $params = ['attemptid' => $attempt->id];
2024
            testable_mod_quiz_external::validate_attempt_review($params);
2025
            $this->fail('Exception expected due missing permissions.');
2026
        } catch (moodle_exception $e) {
2027
            $this->assertEquals('noreviewattempt', $e->errorcode);
2028
        }
2029
    }
2030
 
2031
    /**
2032
     * Test get_attempt_review
2033
     */
2034
    public function test_get_attempt_review(): void {
2035
        global $DB;
2036
 
2037
        // Create a new quiz with two questions and one attempt finished.
2038
        [$quiz, , , $attempt] = $this->create_quiz_with_questions(true, true);
2039
 
2040
        // Add feedback to the quiz.
2041
        $feedback = new \stdClass();
2042
        $feedback->quizid = $quiz->id;
2043
        $feedback->feedbacktext = 'Feedback text 1';
2044
        $feedback->feedbacktextformat = 1;
2045
        $feedback->mingrade = 49;
2046
        $feedback->maxgrade = 100;
2047
        $feedback->id = $DB->insert_record('quiz_feedback', $feedback);
2048
 
2049
        $feedback->feedbacktext = 'Feedback text 2';
2050
        $feedback->feedbacktextformat = 1;
2051
        $feedback->mingrade = 30;
2052
        $feedback->maxgrade = 48;
2053
        $feedback->id = $DB->insert_record('quiz_feedback', $feedback);
2054
 
2055
        $result = mod_quiz_external::get_attempt_review($attempt->id);
2056
        $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_review_returns(), $result);
2057
 
2058
        // Two questions, one completed and correct, the other gave up.
2059
        $this->assertEquals(50, $result['grade']);
2060
        $this->assertEquals(1, $result['attempt']['attempt']);
2061
        $this->assertEquals('finished', $result['attempt']['state']);
2062
        $this->assertEquals(1, $result['attempt']['sumgrades']);
2063
        $this->assertCount(2, $result['questions']);
2064
        $this->assertEquals('gradedright', $result['questions'][0]['state']);
2065
        $this->assertEquals(1, $result['questions'][0]['slot']);
2066
        $this->assertEquals('gaveup', $result['questions'][1]['state']);
2067
        $this->assertEquals(2, $result['questions'][1]['slot']);
2068
 
2069
        // Only first page.
2070
        $result = mod_quiz_external::get_attempt_review($attempt->id, 0);
2071
        $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_review_returns(), $result);
2072
 
2073
        $this->assertEquals(50, $result['grade']);
2074
        $this->assertEquals(1, $result['attempt']['attempt']);
2075
        $this->assertEquals('finished', $result['attempt']['state']);
2076
        $this->assertEquals(1, $result['attempt']['sumgrades']);
2077
        $this->assertCount(1, $result['questions']);
2078
        $this->assertEquals('gradedright', $result['questions'][0]['state']);
2079
        $this->assertEquals(1, $result['questions'][0]['slot']);
2080
 
2081
        $this->assertCount(1, $result['additionaldata']);
2082
        $this->assertEquals('feedback', $result['additionaldata'][0]['id']);
2083
        $this->assertEquals('Feedback', $result['additionaldata'][0]['title']);
2084
        $this->assertEquals('Feedback text 1', $result['additionaldata'][0]['content']);
2085
    }
2086
 
2087
    /**
2088
     * Test get_attempt_review
2089
     */
2090
    public function test_get_attempt_review_with_extra_grades(): void {
2091
        global $DB;
2092
 
2093
        // Create a new quiz with two questions and one attempt finished.
2094
        $this->setUser($this->student);
2095
        [, , , $attempt, $attemptobj] = $this->create_quiz_with_questions(true, true);
2096
 
2097
        // Add some extra grade items.
2098
        $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
2099
        $listeninggrade = $quizgenerator->create_grade_item(['quizid' => $attemptobj->get_quizid(), 'name' => 'Listening']);
2100
        $readinggrade = $quizgenerator->create_grade_item(['quizid' => $attemptobj->get_quizid(), 'name' => 'Reading']);
2101
        $structure = $attemptobj->get_quizobj()->get_structure();
2102
        $structure->update_slot_grade_item($structure->get_slot_by_number(1), $listeninggrade->id);
2103
        $structure->update_slot_grade_item($structure->get_slot_by_number(2), $readinggrade->id);
2104
 
2105
        $result = mod_quiz_external::get_attempt_review($attempt->id);
2106
        $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_review_returns(), $result);
2107
 
2108
        // Two questions, one completed and correct, the other gave up.
2109
        $this->assertEquals(50, $result['grade']);
2110
        $this->assertEquals(1, $result['attempt']['attempt']);
2111
        $this->assertEquals('finished', $result['attempt']['state']);
2112
        $this->assertEquals(1, $result['attempt']['sumgrades']);
2113
        $this->assertCount(2, $result['questions']);
2114
        $this->assertEquals('gradedright', $result['questions'][0]['state']);
2115
        $this->assertEquals(1, $result['questions'][0]['slot']);
2116
        $this->assertEquals('gaveup', $result['questions'][1]['state']);
2117
        $this->assertEquals(2, $result['questions'][1]['slot']);
2118
 
2119
        // Only first page.
2120
        $result = mod_quiz_external::get_attempt_review($attempt->id, 0);
2121
        $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_review_returns(), $result);
2122
 
2123
        $this->assertEquals(50, $result['grade']);
2124
        $this->assertEquals(1, $result['attempt']['attempt']);
2125
        $this->assertEquals('finished', $result['attempt']['state']);
2126
        $this->assertEquals(1, $result['attempt']['sumgrades']);
2127
        $this->assertCount(1, $result['questions']);
2128
        $this->assertEquals('gradedright', $result['questions'][0]['state']);
2129
        $this->assertEquals(1, $result['questions'][0]['slot']);
2130
 
2131
        // Verify additional grades.
2132
        $this->assertEquals(['name' => 'Listening', 'grade' => 1, 'maxgrade' => 1], $result['attempt']['gradeitemmarks'][0]);
2133
        $this->assertEquals(['name' => 'Reading', 'grade' => 0, 'maxgrade' => 1], $result['attempt']['gradeitemmarks'][1]);
2134
 
2135
        // Now change the review options, so marks are not displayed, and check the result.
2136
        $DB->set_field('quiz', 'reviewmarks', 0, ['id' => $attemptobj->get_quizid()]);
2137
        $result = mod_quiz_external::get_attempt_review($attempt->id, 0);
2138
        $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_review_returns(), $result);
2139
 
2140
        $this->assertEquals(1, $result['attempt']['attempt']);
2141
        $this->assertEquals('finished', $result['attempt']['state']);
2142
        $this->assertNull($result['attempt']['sumgrades']);
2143
        $this->assertArrayNotHasKey('gradeitemmarks', $result['attempt']);
2144
    }
2145
 
2146
    /**
2147
     * Test test_view_attempt
2148
     */
11 efrain 2149
    public function test_view_attempt(): void {
1 efrain 2150
        global $DB;
2151
 
2152
        // Create a new quiz with two questions and one attempt started.
2153
        list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true, false);
2154
 
2155
        // Test user with full capabilities.
2156
        $this->setUser($this->student);
2157
 
2158
        // Trigger and capture the event.
2159
        $sink = $this->redirectEvents();
2160
 
2161
        $result = mod_quiz_external::view_attempt($attempt->id, 0);
2162
        $result = external_api::clean_returnvalue(mod_quiz_external::view_attempt_returns(), $result);
2163
        $this->assertTrue($result['status']);
2164
 
2165
        $events = $sink->get_events();
2166
        $this->assertCount(1, $events);
2167
        $event = array_shift($events);
2168
 
2169
        // Checking that the event contains the expected values.
2170
        $this->assertInstanceOf('\mod_quiz\event\attempt_viewed', $event);
2171
        $this->assertEquals($context, $event->get_context());
2172
        $this->assertEventContextNotUsed($event);
2173
        $this->assertNotEmpty($event->get_name());
2174
 
2175
        // Now, force the quiz with QUIZ_NAVMETHOD_SEQ (sequential) navigation method.
2176
        $DB->set_field('quiz', 'navmethod', QUIZ_NAVMETHOD_SEQ, ['id' => $quiz->id]);
2177
        // Quiz requiring preflightdata.
2178
        $DB->set_field('quiz', 'password', 'abcdef', ['id' => $quiz->id]);
2179
        $preflightdata = [["name" => "quizpassword", "value" => 'abcdef']];
2180
 
2181
        // See next page.
2182
        $result = mod_quiz_external::view_attempt($attempt->id, 1, $preflightdata);
2183
        $result = external_api::clean_returnvalue(mod_quiz_external::view_attempt_returns(), $result);
2184
        $this->assertTrue($result['status']);
2185
 
2186
        $events = $sink->get_events();
2187
        $this->assertCount(2, $events);
2188
 
2189
        // Try to go to previous page.
2190
        try {
2191
            mod_quiz_external::view_attempt($attempt->id, 0);
2192
            $this->fail('Exception expected due to try to see a previous page.');
2193
        } catch (moodle_exception $e) {
2194
            $this->assertEquals('Out of sequence access', $e->errorcode);
2195
        }
2196
 
2197
    }
2198
 
2199
    /**
2200
     * Test test_view_attempt_summary
2201
     */
11 efrain 2202
    public function test_view_attempt_summary(): void {
1 efrain 2203
        global $DB;
2204
 
2205
        // Create a new quiz with two questions and one attempt started.
2206
        list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true, false);
2207
 
2208
        // Test user with full capabilities.
2209
        $this->setUser($this->student);
2210
 
2211
        // Trigger and capture the event.
2212
        $sink = $this->redirectEvents();
2213
 
2214
        $result = mod_quiz_external::view_attempt_summary($attempt->id);
2215
        $result = external_api::clean_returnvalue(mod_quiz_external::view_attempt_summary_returns(), $result);
2216
        $this->assertTrue($result['status']);
2217
 
2218
        $events = $sink->get_events();
2219
        $this->assertCount(1, $events);
2220
        $event = array_shift($events);
2221
 
2222
        // Checking that the event contains the expected values.
2223
        $this->assertInstanceOf('\mod_quiz\event\attempt_summary_viewed', $event);
2224
        $this->assertEquals($context, $event->get_context());
2225
        $moodlequiz = new \moodle_url('/mod/quiz/summary.php', ['attempt' => $attempt->id]);
2226
        $this->assertEquals($moodlequiz, $event->get_url());
2227
        $this->assertEventContextNotUsed($event);
2228
        $this->assertNotEmpty($event->get_name());
2229
 
2230
        // Quiz requiring preflightdata.
2231
        $DB->set_field('quiz', 'password', 'abcdef', ['id' => $quiz->id]);
2232
        $preflightdata = [["name" => "quizpassword", "value" => 'abcdef']];
2233
 
2234
        $result = mod_quiz_external::view_attempt_summary($attempt->id, $preflightdata);
2235
        $result = external_api::clean_returnvalue(mod_quiz_external::view_attempt_summary_returns(), $result);
2236
        $this->assertTrue($result['status']);
2237
 
2238
    }
2239
 
2240
    /**
2241
     * Test test_view_attempt_summary
2242
     */
11 efrain 2243
    public function test_view_attempt_review(): void {
1 efrain 2244
        global $DB;
2245
 
2246
        // Create a new quiz with two questions and one attempt finished.
2247
        list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true, true);
2248
 
2249
        // Test user with full capabilities.
2250
        $this->setUser($this->student);
2251
 
2252
        // Trigger and capture the event.
2253
        $sink = $this->redirectEvents();
2254
 
2255
        $result = mod_quiz_external::view_attempt_review($attempt->id, 0);
2256
        $result = external_api::clean_returnvalue(mod_quiz_external::view_attempt_review_returns(), $result);
2257
        $this->assertTrue($result['status']);
2258
 
2259
        $events = $sink->get_events();
2260
        $this->assertCount(1, $events);
2261
        $event = array_shift($events);
2262
 
2263
        // Checking that the event contains the expected values.
2264
        $this->assertInstanceOf('\mod_quiz\event\attempt_reviewed', $event);
2265
        $this->assertEquals($context, $event->get_context());
2266
        $moodlequiz = new \moodle_url('/mod/quiz/review.php', ['attempt' => $attempt->id]);
2267
        $this->assertEquals($moodlequiz, $event->get_url());
2268
        $this->assertEventContextNotUsed($event);
2269
        $this->assertNotEmpty($event->get_name());
2270
 
2271
    }
2272
 
2273
    /**
2274
     * Test get_quiz_feedback_for_grade
2275
     */
11 efrain 2276
    public function test_get_quiz_feedback_for_grade(): void {
1 efrain 2277
        global $DB;
2278
 
2279
        // Add feedback to the quiz.
2280
        $feedback = new \stdClass();
2281
        $feedback->quizid = $this->quiz->id;
2282
        $feedback->feedbacktext = 'Feedback text 1';
2283
        $feedback->feedbacktextformat = 1;
2284
        $feedback->mingrade = 49;
2285
        $feedback->maxgrade = 100;
2286
        $feedback->id = $DB->insert_record('quiz_feedback', $feedback);
2287
        // Add a fake inline image to the feedback text.
2288
        $filename = 'shouldbeanimage.jpg';
2289
        $filerecordinline = [
2290
            'contextid' => $this->context->id,
2291
            'component' => 'mod_quiz',
2292
            'filearea'  => 'feedback',
2293
            'itemid'    => $feedback->id,
2294
            'filepath'  => '/',
2295
            'filename'  => $filename,
2296
        ];
2297
        $fs = get_file_storage();
2298
        $fs->create_file_from_string($filerecordinline, 'image contents (not really)');
2299
 
2300
        $feedback->feedbacktext = 'Feedback text 2';
2301
        $feedback->feedbacktextformat = 1;
2302
        $feedback->mingrade = 30;
2303
        $feedback->maxgrade = 49;
2304
        $feedback->id = $DB->insert_record('quiz_feedback', $feedback);
2305
 
2306
        $result = mod_quiz_external::get_quiz_feedback_for_grade($this->quiz->id, 50);
2307
        $result = external_api::clean_returnvalue(mod_quiz_external::get_quiz_feedback_for_grade_returns(), $result);
2308
        $this->assertEquals('Feedback text 1', $result['feedbacktext']);
2309
        $this->assertEquals($filename, $result['feedbackinlinefiles'][0]['filename']);
2310
        $this->assertEquals(FORMAT_HTML, $result['feedbacktextformat']);
2311
 
2312
        $result = mod_quiz_external::get_quiz_feedback_for_grade($this->quiz->id, 30);
2313
        $result = external_api::clean_returnvalue(mod_quiz_external::get_quiz_feedback_for_grade_returns(), $result);
2314
        $this->assertEquals('Feedback text 2', $result['feedbacktext']);
2315
        $this->assertEquals(FORMAT_HTML, $result['feedbacktextformat']);
2316
 
2317
        $result = mod_quiz_external::get_quiz_feedback_for_grade($this->quiz->id, 10);
2318
        $result = external_api::clean_returnvalue(mod_quiz_external::get_quiz_feedback_for_grade_returns(), $result);
2319
        $this->assertEquals('', $result['feedbacktext']);
2320
        $this->assertEquals(FORMAT_MOODLE, $result['feedbacktextformat']);
2321
    }
2322
 
2323
    /**
2324
     * Test get_quiz_access_information
2325
     */
11 efrain 2326
    public function test_get_quiz_access_information(): void {
1 efrain 2327
        global $DB;
2328
 
2329
        // Create a new quiz.
2330
        $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
2331
        $data = ['course' => $this->course->id];
2332
        $quiz = $quizgenerator->create_instance($data);
2333
 
2334
        $this->setUser($this->student);
2335
 
2336
        // Default restrictions (none).
2337
        $result = mod_quiz_external::get_quiz_access_information($quiz->id);
2338
        $result = external_api::clean_returnvalue(mod_quiz_external::get_quiz_access_information_returns(), $result);
2339
 
2340
        $expected = [
2341
            'canattempt' => true,
2342
            'canmanage' => false,
2343
            'canpreview' => false,
2344
            'canreviewmyattempts' => true,
2345
            'canviewreports' => false,
2346
            'accessrules' => [],
2347
            // This rule is always used, even if the quiz has no open or close date.
2348
            'activerulenames' => ['quizaccess_openclosedate'],
2349
            'preventaccessreasons' => [],
2350
            'warnings' => []
2351
        ];
2352
 
2353
        $this->assertEquals($expected, $result);
2354
 
2355
        // Now teacher, different privileges.
2356
        $this->setUser($this->teacher);
2357
        $result = mod_quiz_external::get_quiz_access_information($quiz->id);
2358
        $result = external_api::clean_returnvalue(mod_quiz_external::get_quiz_access_information_returns(), $result);
2359
 
2360
        $expected['canmanage'] = true;
2361
        $expected['canpreview'] = true;
2362
        $expected['canviewreports'] = true;
2363
        $expected['canattempt'] = false;
2364
        $expected['canreviewmyattempts'] = false;
2365
 
2366
        $this->assertEquals($expected, $result);
2367
 
2368
        $this->setUser($this->student);
2369
        // Now add some restrictions.
2370
        $quiz->timeopen = time() + DAYSECS;
2371
        $quiz->timeclose = time() + WEEKSECS;
2372
        $quiz->password = '123456';
2373
        $DB->update_record('quiz', $quiz);
2374
 
2375
        $result = mod_quiz_external::get_quiz_access_information($quiz->id);
2376
        $result = external_api::clean_returnvalue(mod_quiz_external::get_quiz_access_information_returns(), $result);
2377
 
2378
        // Access is limited by time and password, but only the password limit has a description.
2379
        $this->assertCount(1, $result['accessrules']);
2380
        // Two rule names, password and open/close date.
2381
        $this->assertCount(2, $result['activerulenames']);
2382
        $this->assertCount(1, $result['preventaccessreasons']);
2383
 
2384
    }
2385
 
2386
    /**
2387
     * Test get_attempt_access_information
2388
     */
11 efrain 2389
    public function test_get_attempt_access_information(): void {
1 efrain 2390
        global $DB;
2391
 
2392
        $this->setAdminUser();
2393
 
2394
        // Create a new quiz with attempts.
2395
        $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
2396
        $data = ['course' => $this->course->id,
2397
                      'sumgrades' => 2];
2398
        $quiz = $quizgenerator->create_instance($data);
2399
 
2400
        // Create some questions.
2401
        $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
2402
 
2403
        $cat = $questiongenerator->create_question_category();
2404
        $question = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]);
2405
        quiz_add_quiz_question($question->id, $quiz);
2406
 
2407
        $question = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
2408
        quiz_add_quiz_question($question->id, $quiz);
2409
 
2410
        // Add new question types in the category (for the random one).
2411
        $question = $questiongenerator->create_question('truefalse', null, ['category' => $cat->id]);
2412
        $question = $questiongenerator->create_question('essay', null, ['category' => $cat->id]);
2413
 
2414
        $this->add_random_questions($quiz->id, 0, $cat->id, 1);
2415
 
2416
        $quizobj = quiz_settings::create($quiz->id, $this->student->id);
2417
 
2418
        // Set grade to pass.
2419
        $item = \grade_item::fetch(['courseid' => $this->course->id, 'itemtype' => 'mod',
2420
                                        'itemmodule' => 'quiz', 'iteminstance' => $quiz->id, 'outcomeid' => null]);
2421
        $item->gradepass = 80;
2422
        $item->update();
2423
 
2424
        $this->setUser($this->student);
2425
 
2426
        // Default restrictions (none).
2427
        $result = mod_quiz_external::get_attempt_access_information($quiz->id);
2428
        $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_access_information_returns(), $result);
2429
 
2430
        $expected = [
2431
            'isfinished' => false,
2432
            'preventnewattemptreasons' => [],
2433
            'warnings' => []
2434
        ];
2435
 
2436
        $this->assertEquals($expected, $result);
2437
 
2438
        // Limited attempts.
2439
        $quiz->attempts = 1;
2440
        $DB->update_record('quiz', $quiz);
2441
 
2442
        // Now, do one attempt.
2443
        $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
2444
        $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
2445
 
2446
        $timenow = time();
2447
        $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $this->student->id);
2448
        quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
2449
        quiz_attempt_save_started($quizobj, $quba, $attempt);
2450
 
2451
        // Process some responses from the student.
2452
        $attemptobj = quiz_attempt::create($attempt->id);
2453
        $tosubmit = [1 => ['answer' => '3.14']];
2454
        $attemptobj->process_submitted_actions($timenow, false, $tosubmit);
2455
 
2456
        // Finish the attempt.
2457
        $attemptobj = quiz_attempt::create($attempt->id);
2458
        $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
1441 ariadna 2459
        $attemptobj->process_submit($timenow, false);
2460
        $attemptobj->process_grade_submission($timenow);
1 efrain 2461
 
2462
        // Can we start a new attempt? We shall not!
2463
        $result = mod_quiz_external::get_attempt_access_information($quiz->id, $attempt->id);
2464
        $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_access_information_returns(), $result);
2465
 
2466
        // Now new attemps allowed.
2467
        $this->assertCount(1, $result['preventnewattemptreasons']);
2468
        $this->assertFalse($result['ispreflightcheckrequired']);
2469
        $this->assertEquals(get_string('nomoreattempts', 'quiz'), $result['preventnewattemptreasons'][0]);
2470
 
2471
    }
2472
 
2473
    /**
2474
     * Test get_quiz_required_qtypes
2475
     */
11 efrain 2476
    public function test_get_quiz_required_qtypes(): void {
1 efrain 2477
        $this->setAdminUser();
2478
 
2479
        // Create a new quiz.
2480
        $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
2481
        $data = ['course' => $this->course->id];
2482
        $quiz = $quizgenerator->create_instance($data);
2483
 
2484
        // Create some questions.
2485
        $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
2486
 
2487
        $cat = $questiongenerator->create_question_category();
2488
        $question = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]);
2489
        quiz_add_quiz_question($question->id, $quiz);
2490
 
2491
        $question = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
2492
        quiz_add_quiz_question($question->id, $quiz);
2493
 
2494
        $question = $questiongenerator->create_question('truefalse', null, ['category' => $cat->id]);
2495
        quiz_add_quiz_question($question->id, $quiz);
2496
 
2497
        $question = $questiongenerator->create_question('essay', null, ['category' => $cat->id]);
2498
        quiz_add_quiz_question($question->id, $quiz);
2499
 
2500
        $question = $questiongenerator->create_question('multichoice', null,
2501
                ['category' => $cat->id, 'status' => question_version_status::QUESTION_STATUS_DRAFT]);
2502
        quiz_add_quiz_question($question->id, $quiz);
2503
 
2504
        $this->setUser($this->student);
2505
 
2506
        $result = mod_quiz_external::get_quiz_required_qtypes($quiz->id);
2507
        $result = external_api::clean_returnvalue(mod_quiz_external::get_quiz_required_qtypes_returns(), $result);
2508
 
2509
        $expected = [
2510
            'questiontypes' => ['essay', 'numerical', 'shortanswer', 'truefalse'],
2511
            'warnings' => []
2512
        ];
2513
 
2514
        $this->assertEquals($expected, $result);
2515
 
2516
    }
2517
 
2518
    /**
2519
     * Test get_quiz_required_qtypes for quiz with random questions
2520
     */
11 efrain 2521
    public function test_get_quiz_required_qtypes_random(): void {
1 efrain 2522
        $this->setAdminUser();
2523
 
2524
        // Create a new quiz.
2525
        $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
2526
        $quiz = $quizgenerator->create_instance(['course' => $this->course->id]);
2527
 
2528
        // Create some questions.
2529
        $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
2530
 
2531
        $cat = $questiongenerator->create_question_category();
2532
        $anothercat = $questiongenerator->create_question_category();
2533
 
2534
        $question = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]);
2535
        $question = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
2536
        $question = $questiongenerator->create_question('truefalse', null, ['category' => $cat->id]);
2537
        // Question in a different category.
2538
        $question = $questiongenerator->create_question('essay', null, ['category' => $anothercat->id]);
2539
 
2540
        // Add a couple of random questions from the same category.
2541
        $this->add_random_questions($quiz->id, 0, $cat->id, 1);
2542
        $this->add_random_questions($quiz->id, 0, $cat->id, 1);
2543
 
2544
        $this->setUser($this->student);
2545
 
2546
        $result = mod_quiz_external::get_quiz_required_qtypes($quiz->id);
2547
        $result = external_api::clean_returnvalue(mod_quiz_external::get_quiz_required_qtypes_returns(), $result);
2548
 
2549
        $expected = ['numerical', 'shortanswer', 'truefalse'];
2550
        ksort($result['questiontypes']);
2551
 
2552
        $this->assertEquals($expected, $result['questiontypes']);
2553
 
2554
        // Add more questions to the quiz, this time from the other category.
2555
        $this->setAdminUser();
2556
        $this->add_random_questions($quiz->id, 0, $anothercat->id, 1);
2557
 
2558
        $this->setUser($this->student);
2559
        $result = mod_quiz_external::get_quiz_required_qtypes($quiz->id);
2560
        $result = external_api::clean_returnvalue(mod_quiz_external::get_quiz_required_qtypes_returns(), $result);
2561
 
2562
        // The new question from the new category is returned as a potential random question for the quiz.
2563
        $expected = ['essay', 'numerical', 'shortanswer', 'truefalse'];
2564
        ksort($result['questiontypes']);
2565
 
2566
        $this->assertEquals($expected, $result['questiontypes']);
2567
    }
2568
 
2569
    /**
2570
     * Test that a sequential navigation quiz is not allowing to see questions in advance except if reviewing
2571
     */
11 efrain 2572
    public function test_sequential_navigation_view_attempt(): void {
1 efrain 2573
        // Test user with full capabilities.
2574
        $quiz = $this->prepare_sequential_quiz();
2575
        $attemptobj = $this->create_quiz_attempt_object($quiz);
2576
        $this->setUser($this->student);
2577
        // Check out of sequence access for view.
2578
        $this->assertNotEmpty(mod_quiz_external::view_attempt($attemptobj->get_attemptid(), 0, []));
2579
        try {
2580
            mod_quiz_external::view_attempt($attemptobj->get_attemptid(), 3, []);
2581
            $this->fail('Exception expected due to out of sequence access.');
2582
        } catch (moodle_exception $e) {
2583
            $this->assertStringContainsString('quiz/Out of sequence access', $e->getMessage());
2584
        }
2585
    }
2586
 
2587
    /**
2588
     * Test that a sequential navigation quiz is not allowing to see questions content in advance for a student.
2589
     */
11 efrain 2590
    public function test_sequential_navigation_attempt_summary(): void {
1 efrain 2591
        // Test user with full capabilities.
2592
        $quiz = $this->prepare_sequential_quiz();
2593
        $attemptobj = $this->create_quiz_attempt_object($quiz);
2594
        $this->setUser($this->student);
2595
        // Check that we do not return content from other questions except than the ones currently viewed.
2596
        $result = mod_quiz_external::get_attempt_summary($attemptobj->get_attemptid());
2597
        $this->assertStringContainsString('Question (1)', $result['questions'][0]['html']); // Current question.
2598
        $this->assertEmpty($result['questions'][1]['html']); // Next question.
2599
        $this->assertEmpty($result['questions'][2]['html']); // And more.
2600
        $this->assertEmpty($result['questions'][3]['html']); // And more.
2601
        $this->assertEmpty($result['questions'][4]['html']); // And more.
2602
        $this->assertNotContains('totalunanswered', $result);   // For sequential quizzes, unanswered questions are not considered.
2603
    }
2604
 
2605
    /**
2606
     * Test that a sequential navigation quiz is not allowing to see questions in advance for student
2607
     */
11 efrain 2608
    public function test_sequential_navigation_get_attempt_data(): void {
1 efrain 2609
        // Test user with full capabilities.
2610
        $quiz = $this->prepare_sequential_quiz();
2611
        $attemptobj = $this->create_quiz_attempt_object($quiz);
2612
        $this->setUser($this->student);
2613
        // Test invalid instance id.
2614
        try {
2615
            mod_quiz_external::get_attempt_data($attemptobj->get_attemptid(), 2);
2616
            $this->fail('Exception expected due to out of sequence access.');
2617
        } catch (moodle_exception $e) {
2618
            $this->assertStringContainsString('quiz/Out of sequence access', $e->getMessage());
2619
        }
2620
        // Now we moved to page 1, we should see page 2 and 1 but not 0 or 3.
2621
        $attemptobj->set_currentpage(1);
2622
        // Test invalid instance id.
2623
        try {
2624
            mod_quiz_external::get_attempt_data($attemptobj->get_attemptid(), 0);
2625
            $this->fail('Exception expected due to out of sequence access.');
2626
        } catch (moodle_exception $e) {
2627
            $this->assertStringContainsString('quiz/Out of sequence access', $e->getMessage());
2628
        }
2629
 
2630
        try {
2631
            mod_quiz_external::get_attempt_data($attemptobj->get_attemptid(), 3);
2632
            $this->fail('Exception expected due to out of sequence access.');
2633
        } catch (moodle_exception $e) {
2634
            $this->assertStringContainsString('quiz/Out of sequence access', $e->getMessage());
2635
        }
2636
 
2637
        // Now we can see page 1.
2638
        $result = mod_quiz_external::get_attempt_data($attemptobj->get_attemptid(), 1);
2639
        $this->assertCount(1, $result['questions']);
2640
        $this->assertStringContainsString('Question (2)', $result['questions'][0]['html']);
2641
    }
2642
 
2643
    /**
2644
     * Prepare quiz for sequential navigation tests
2645
     *
2646
     * @return quiz_settings
2647
     */
2648
    private function prepare_sequential_quiz(): quiz_settings {
2649
        // Create a new quiz with 5 questions and one attempt started.
2650
        // Create a new quiz with attempts.
2651
        $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
2652
        $data = [
2653
            'course' => $this->course->id,
2654
            'sumgrades' => 2,
2655
            'questionsperpage' => 1,
2656
            'preferredbehaviour' => 'deferredfeedback',
2657
            'navmethod' => QUIZ_NAVMETHOD_SEQ
2658
        ];
2659
        $quiz = $quizgenerator->create_instance($data);
2660
 
2661
        // Now generate the questions.
2662
        $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
2663
        $cat = $questiongenerator->create_question_category();
2664
        for ($pageindex = 1; $pageindex <= 5; $pageindex++) {
2665
            $question = $questiongenerator->create_question('truefalse', null, [
2666
                'category' => $cat->id,
2667
                'questiontext' => ['text' => "Question ($pageindex)"]
2668
            ]);
2669
            quiz_add_quiz_question($question->id, $quiz, $pageindex);
2670
        }
2671
 
2672
        $quizobj = quiz_settings::create($quiz->id, $this->student->id);
2673
        // Set grade to pass.
2674
        $item = \grade_item::fetch(['courseid' => $this->course->id, 'itemtype' => 'mod',
2675
            'itemmodule' => 'quiz', 'iteminstance' => $quiz->id, 'outcomeid' => null]);
2676
        $item->gradepass = 80;
2677
        $item->update();
2678
        return $quizobj;
2679
    }
2680
 
2681
    /**
2682
     * Create question attempt
2683
     *
2684
     * @param quiz_settings $quizobj
2685
     * @param int|null $userid
2686
     * @param bool|null $ispreview
2687
     * @return quiz_attempt
2688
     * @throws moodle_exception
2689
     */
2690
    private function create_quiz_attempt_object(
2691
        quiz_settings $quizobj,
2692
        ?int $userid = null,
2693
        ?bool $ispreview = false
2694
    ): quiz_attempt {
2695
        global $USER;
2696
 
2697
        $timenow = time();
2698
        // Now, do one attempt.
2699
        $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
2700
        $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
2701
        $attemptnumber = 1;
2702
        if (!empty($USER->id)) {
2703
            $attemptnumber = count(quiz_get_user_attempts($quizobj->get_quizid(), $USER->id)) + 1;
2704
        }
2705
        $attempt = quiz_create_attempt($quizobj, $attemptnumber, false, $timenow, $ispreview, $userid ?? $this->student->id);
2706
        quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $timenow);
2707
        quiz_attempt_save_started($quizobj, $quba, $attempt);
2708
        $attemptobj = quiz_attempt::create($attempt->id);
2709
        return $attemptobj;
2710
    }
2711
}