Merge branch 'MDL-52888-master' of git://github.com/jleyva/moodle
[moodle.git] / mod / quiz / tests / external_test.php
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/>.
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  */
27 defined('MOODLE_INTERNAL') || die();
29 global $CFG;
31 require_once($CFG->dirroot . '/webservice/tests/helpers.php');
33 /**
34  * Silly class to access mod_quiz_external internal methods.
35  *
36  * @package mod_quiz
37  * @copyright 2016 Juan Leyva <juan@moodle.com>
38  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
39  * @since  Moodle 3.1
40  */
41 class testable_mod_quiz_external extends mod_quiz_external {
43     /**
44      * Public accessor.
45      *
46      * @param  array $params Array of parameters including the attemptid and preflight data
47      * @param  bool $checkaccessrules whether to check the quiz access rules or not
48      * @param  bool $failifoverdue whether to return error if the attempt is overdue
49      * @return  array containing the attempt object and access messages
50      */
51     public static function validate_attempt($params, $checkaccessrules = true, $failifoverdue = true) {
52         return parent::validate_attempt($params, $checkaccessrules, $failifoverdue);
53     }
55     /**
56      * Public accessor.
57      *
58      * @param  array $params Array of parameters including the attemptid
59      * @return  array containing the attempt object and display options
60      */
61     public static function validate_attempt_review($params) {
62         return parent::validate_attempt_review($params);
63     }
64 }
66 /**
67  * Quiz module external functions tests
68  *
69  * @package    mod_quiz
70  * @category   external
71  * @copyright  2016 Juan Leyva <juan@moodle.com>
72  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
73  * @since      Moodle 3.1
74  */
75 class mod_quiz_external_testcase extends externallib_advanced_testcase {
77     /**
78      * Set up for every test
79      */
80     public function setUp() {
81         global $DB;
82         $this->resetAfterTest();
83         $this->setAdminUser();
85         // Setup test data.
86         $this->course = $this->getDataGenerator()->create_course();
87         $this->quiz = $this->getDataGenerator()->create_module('quiz', array('course' => $this->course->id));
88         $this->context = context_module::instance($this->quiz->cmid);
89         $this->cm = get_coursemodule_from_instance('quiz', $this->quiz->id);
91         // Create users.
92         $this->student = self::getDataGenerator()->create_user();
93         $this->teacher = self::getDataGenerator()->create_user();
95         // Users enrolments.
96         $this->studentrole = $DB->get_record('role', array('shortname' => 'student'));
97         $this->teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
98         $this->getDataGenerator()->enrol_user($this->student->id, $this->course->id, $this->studentrole->id, 'manual');
99         $this->getDataGenerator()->enrol_user($this->teacher->id, $this->course->id, $this->teacherrole->id, 'manual');
100     }
102     /**
103      * Create a quiz with questions including a started or finished attempt optionally
104      *
105      * @param  boolean $startattempt whether to start a new attempt
106      * @param  boolean $finishattempt whether to finish the new attempt
107      * @return array array containing the quiz, context and the attempt
108      */
109     private function create_quiz_with_questions($startattempt = false, $finishattempt = false) {
111         // Create a new quiz with attempts.
112         $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
113         $data = array('course' => $this->course->id,
114                       'sumgrades' => 2);
115         $quiz = $quizgenerator->create_instance($data);
116         $context = context_module::instance($quiz->cmid);
118         // Create a couple of questions.
119         $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
121         $cat = $questiongenerator->create_question_category();
122         $question = $questiongenerator->create_question('numerical', null, array('category' => $cat->id));
123         quiz_add_quiz_question($question->id, $quiz);
124         $question = $questiongenerator->create_question('numerical', null, array('category' => $cat->id));
125         quiz_add_quiz_question($question->id, $quiz);
127         $quizobj = quiz::create($quiz->id, $this->student->id);
129         // Set grade to pass.
130         $item = grade_item::fetch(array('courseid' => $this->course->id, 'itemtype' => 'mod',
131                                         'itemmodule' => 'quiz', 'iteminstance' => $quiz->id, 'outcomeid' => null));
132         $item->gradepass = 80;
133         $item->update();
135         if ($startattempt or $finishattempt) {
136             // Now, do one attempt.
137             $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
138             $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
140             $timenow = time();
141             $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $this->student->id);
142             quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
143             quiz_attempt_save_started($quizobj, $quba, $attempt);
144             $attemptobj = quiz_attempt::create($attempt->id);
146             if ($finishattempt) {
147                 // Process some responses from the student.
148                 $tosubmit = array(1 => array('answer' => '3.14'));
149                 $attemptobj->process_submitted_actions(time(), false, $tosubmit);
151                 // Finish the attempt.
152                 $attemptobj->process_finish(time(), false);
153             }
154             return array($quiz, $context, $quizobj, $attempt, $attemptobj, $quba);
155         } else {
156             return array($quiz, $context, $quizobj);
157         }
159     }
161     /*
162      * Test get quizzes by courses
163      */
164     public function test_mod_quiz_get_quizzes_by_courses() {
165         global $DB;
167         // Create additional course.
168         $course2 = self::getDataGenerator()->create_course();
170         // Second quiz.
171         $record = new stdClass();
172         $record->course = $course2->id;
173         $quiz2 = self::getDataGenerator()->create_module('quiz', $record);
175         // Execute real Moodle enrolment as we'll call unenrol() method on the instance later.
176         $enrol = enrol_get_plugin('manual');
177         $enrolinstances = enrol_get_instances($course2->id, true);
178         foreach ($enrolinstances as $courseenrolinstance) {
179             if ($courseenrolinstance->enrol == "manual") {
180                 $instance2 = $courseenrolinstance;
181                 break;
182             }
183         }
184         $enrol->enrol_user($instance2, $this->student->id, $this->studentrole->id);
186         self::setUser($this->student);
188         $returndescription = mod_quiz_external::get_quizzes_by_courses_returns();
190         // Create what we expect to be returned when querying the two courses.
191         // First for the student user.
192         $allusersfields = array('id', 'coursemodule', 'course', 'name', 'intro', 'introformat', 'timeopen', 'timeclose',
193                                 'grademethod', 'section', 'visible', 'groupmode', 'groupingid');
194         $userswithaccessfields = array('timelimit', 'attempts', 'attemptonlast', 'grademethod', 'decimalpoints',
195                                         'questiondecimalpoints', 'reviewattempt', 'reviewcorrectness', 'reviewmarks',
196                                         'reviewspecificfeedback', 'reviewgeneralfeedback', 'reviewrightanswer',
197                                         'reviewoverallfeedback', 'questionsperpage', 'navmethod', 'sumgrades', 'grade',
198                                         'browsersecurity', 'delay1', 'delay2', 'showuserpicture', 'showblocks',
199                                         'completionattemptsexhausted', 'completionpass', 'autosaveperiod', 'hasquestions',
200                                         'hasfeedback', 'overduehandling', 'graceperiod', 'preferredbehaviour', 'canredoquestions');
201         $managerfields = array('shuffleanswers', 'timecreated', 'timemodified', 'password', 'subnet');
203         // Add expected coursemodule and other data.
204         $quiz1 = $this->quiz;
205         $quiz1->coursemodule = $quiz1->cmid;
206         $quiz1->introformat = 1;
207         $quiz1->section = 0;
208         $quiz1->visible = true;
209         $quiz1->groupmode = 0;
210         $quiz1->groupingid = 0;
211         $quiz1->hasquestions = 0;
212         $quiz1->hasfeedback = 0;
213         $quiz1->autosaveperiod = get_config('quiz', 'autosaveperiod');
215         $quiz2->coursemodule = $quiz2->cmid;
216         $quiz2->introformat = 1;
217         $quiz2->section = 0;
218         $quiz2->visible = true;
219         $quiz2->groupmode = 0;
220         $quiz2->groupingid = 0;
221         $quiz2->hasquestions = 0;
222         $quiz2->hasfeedback = 0;
223         $quiz2->autosaveperiod = get_config('quiz', 'autosaveperiod');
225         foreach (array_merge($allusersfields, $userswithaccessfields) as $field) {
226             $expected1[$field] = $quiz1->{$field};
227             $expected2[$field] = $quiz2->{$field};
228         }
230         $expectedquizzes = array($expected2, $expected1);
232         // Call the external function passing course ids.
233         $result = mod_quiz_external::get_quizzes_by_courses(array($course2->id, $this->course->id));
234         $result = external_api::clean_returnvalue($returndescription, $result);
236         $this->assertEquals($expectedquizzes, $result['quizzes']);
237         $this->assertCount(0, $result['warnings']);
239         // Call the external function without passing course id.
240         $result = mod_quiz_external::get_quizzes_by_courses();
241         $result = external_api::clean_returnvalue($returndescription, $result);
242         $this->assertEquals($expectedquizzes, $result['quizzes']);
243         $this->assertCount(0, $result['warnings']);
245         // Unenrol user from second course and alter expected quizzes.
246         $enrol->unenrol_user($instance2, $this->student->id);
247         array_shift($expectedquizzes);
249         // Call the external function without passing course id.
250         $result = mod_quiz_external::get_quizzes_by_courses();
251         $result = external_api::clean_returnvalue($returndescription, $result);
252         $this->assertEquals($expectedquizzes, $result['quizzes']);
254         // Call for the second course we unenrolled the user from, expected warning.
255         $result = mod_quiz_external::get_quizzes_by_courses(array($course2->id));
256         $this->assertCount(1, $result['warnings']);
257         $this->assertEquals('1', $result['warnings'][0]['warningcode']);
258         $this->assertEquals($course2->id, $result['warnings'][0]['itemid']);
260         // Now, try as a teacher for getting all the additional fields.
261         self::setUser($this->teacher);
263         foreach ($managerfields as $field) {
264             $expectedquizzes[0][$field] = $quiz1->{$field};
265         }
267         $result = mod_quiz_external::get_quizzes_by_courses();
268         $result = external_api::clean_returnvalue($returndescription, $result);
269         $this->assertEquals($expectedquizzes, $result['quizzes']);
271         // Admin also should get all the information.
272         self::setAdminUser();
274         $result = mod_quiz_external::get_quizzes_by_courses(array($this->course->id));
275         $result = external_api::clean_returnvalue($returndescription, $result);
276         $this->assertEquals($expectedquizzes, $result['quizzes']);
278         // Now, prevent access.
279         $enrol->enrol_user($instance2, $this->student->id);
281         self::setUser($this->student);
283         $quiz2->timeclose = time() - DAYSECS;
284         $DB->update_record('quiz', $quiz2);
286         $result = mod_quiz_external::get_quizzes_by_courses();
287         $result = external_api::clean_returnvalue($returndescription, $result);
288         $this->assertCount(2, $result['quizzes']);
289         // We only see a limited set of fields.
290         $this->assertCount(4, $result['quizzes'][0]);
291         $this->assertEquals($quiz2->id, $result['quizzes'][0]['id']);
292         $this->assertEquals($quiz2->coursemodule, $result['quizzes'][0]['coursemodule']);
293         $this->assertEquals($quiz2->course, $result['quizzes'][0]['course']);
294         $this->assertEquals($quiz2->name, $result['quizzes'][0]['name']);
295         $this->assertEquals($quiz2->course, $result['quizzes'][0]['course']);
297         $this->assertFalse(isset($result['quizzes'][0]['timelimit']));
299     }
301     /**
302      * Test test_view_quiz
303      */
304     public function test_view_quiz() {
305         global $DB;
307         // Test invalid instance id.
308         try {
309             mod_quiz_external::view_quiz(0);
310             $this->fail('Exception expected due to invalid mod_quiz instance id.');
311         } catch (moodle_exception $e) {
312             $this->assertEquals('invalidrecord', $e->errorcode);
313         }
315         // Test not-enrolled user.
316         $usernotenrolled = self::getDataGenerator()->create_user();
317         $this->setUser($usernotenrolled);
318         try {
319             mod_quiz_external::view_quiz($this->quiz->id);
320             $this->fail('Exception expected due to not enrolled user.');
321         } catch (moodle_exception $e) {
322             $this->assertEquals('requireloginerror', $e->errorcode);
323         }
325         // Test user with full capabilities.
326         $this->setUser($this->student);
328         // Trigger and capture the event.
329         $sink = $this->redirectEvents();
331         $result = mod_quiz_external::view_quiz($this->quiz->id);
332         $result = external_api::clean_returnvalue(mod_quiz_external::view_quiz_returns(), $result);
333         $this->assertTrue($result['status']);
335         $events = $sink->get_events();
336         $this->assertCount(1, $events);
337         $event = array_shift($events);
339         // Checking that the event contains the expected values.
340         $this->assertInstanceOf('\mod_quiz\event\course_module_viewed', $event);
341         $this->assertEquals($this->context, $event->get_context());
342         $moodlequiz = new \moodle_url('/mod/quiz/view.php', array('id' => $this->cm->id));
343         $this->assertEquals($moodlequiz, $event->get_url());
344         $this->assertEventContextNotUsed($event);
345         $this->assertNotEmpty($event->get_name());
347         // Test user with no capabilities.
348         // We need a explicit prohibit since this capability is only defined in authenticated user and guest roles.
349         assign_capability('mod/quiz:view', CAP_PROHIBIT, $this->studentrole->id, $this->context->id);
350         // Empty all the caches that may be affected  by this change.
351         accesslib_clear_all_caches_for_unit_testing();
352         course_modinfo::clear_instance_cache();
354         try {
355             mod_quiz_external::view_quiz($this->quiz->id);
356             $this->fail('Exception expected due to missing capability.');
357         } catch (moodle_exception $e) {
358             $this->assertEquals('requireloginerror', $e->errorcode);
359         }
361     }
363     /**
364      * Test get_user_attempts
365      */
366     public function test_get_user_attempts() {
368         // Create a quiz with one attempt finished.
369         list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true, true);
371         $this->setUser($this->student);
372         $result = mod_quiz_external::get_user_attempts($quiz->id);
373         $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
375         $this->assertCount(1, $result['attempts']);
376         $this->assertEquals($attempt->id, $result['attempts'][0]['id']);
377         $this->assertEquals($quiz->id, $result['attempts'][0]['quiz']);
378         $this->assertEquals($this->student->id, $result['attempts'][0]['userid']);
379         $this->assertEquals(1, $result['attempts'][0]['attempt']);
381         // Test filters. Only finished.
382         $result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'finished', false);
383         $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
385         $this->assertCount(1, $result['attempts']);
386         $this->assertEquals($attempt->id, $result['attempts'][0]['id']);
388         // Test filters. All attempts.
389         $result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'all', false);
390         $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
392         $this->assertCount(1, $result['attempts']);
393         $this->assertEquals($attempt->id, $result['attempts'][0]['id']);
395         // Test filters. Unfinished.
396         $result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'unfinished', false);
397         $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
399         $this->assertCount(0, $result['attempts']);
401         // Start a new attempt, but not finish it.
402         $timenow = time();
403         $attempt = quiz_create_attempt($quizobj, 2, false, $timenow, false, $this->student->id);
404         $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
405         $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
407         quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
408         quiz_attempt_save_started($quizobj, $quba, $attempt);
410         // Test filters. All attempts.
411         $result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'all', false);
412         $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
414         $this->assertCount(2, $result['attempts']);
416         // Test filters. Unfinished.
417         $result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'unfinished', false);
418         $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
420         $this->assertCount(1, $result['attempts']);
422         // Test manager can see user attempts.
423         $this->setUser($this->teacher);
424         $result = mod_quiz_external::get_user_attempts($quiz->id, $this->student->id);
425         $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
427         $this->assertCount(1, $result['attempts']);
428         $this->assertEquals($this->student->id, $result['attempts'][0]['userid']);
430         $result = mod_quiz_external::get_user_attempts($quiz->id, $this->student->id, 'all');
431         $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result);
433         $this->assertCount(2, $result['attempts']);
434         $this->assertEquals($this->student->id, $result['attempts'][0]['userid']);
436         // Invalid parameters.
437         try {
438             mod_quiz_external::get_user_attempts($quiz->id, $this->student->id, 'INVALID_PARAMETER');
439             $this->fail('Exception expected due to missing capability.');
440         } catch (invalid_parameter_exception $e) {
441             $this->assertEquals('invalidparameter', $e->errorcode);
442         }
443     }
445     /**
446      * Test get_user_best_grade
447      */
448     public function test_get_user_best_grade() {
449         global $DB;
451         $this->setUser($this->student);
453         $result = mod_quiz_external::get_user_best_grade($this->quiz->id);
454         $result = external_api::clean_returnvalue(mod_quiz_external::get_user_best_grade_returns(), $result);
456         // No grades yet.
457         $this->assertFalse($result['hasgrade']);
458         $this->assertTrue(!isset($result['grade']));
460         $grade = new stdClass();
461         $grade->quiz = $this->quiz->id;
462         $grade->userid = $this->student->id;
463         $grade->grade = 8.9;
464         $grade->timemodified = time();
465         $grade->id = $DB->insert_record('quiz_grades', $grade);
467         $result = mod_quiz_external::get_user_best_grade($this->quiz->id);
468         $result = external_api::clean_returnvalue(mod_quiz_external::get_user_best_grade_returns(), $result);
470         // Now I have grades.
471         $this->assertTrue($result['hasgrade']);
472         $this->assertEquals(8.9, $result['grade']);
474         // We should not see other users grades.
475         $anotherstudent = self::getDataGenerator()->create_user();
476         $this->getDataGenerator()->enrol_user($anotherstudent->id, $this->course->id, $this->studentrole->id, 'manual');
478         try {
479             mod_quiz_external::get_user_best_grade($this->quiz->id, $anotherstudent->id);
480             $this->fail('Exception expected due to missing capability.');
481         } catch (required_capability_exception $e) {
482             $this->assertEquals('nopermissions', $e->errorcode);
483         }
485         // Teacher must be able to see student grades.
486         $this->setUser($this->teacher);
488         $result = mod_quiz_external::get_user_best_grade($this->quiz->id, $this->student->id);
489         $result = external_api::clean_returnvalue(mod_quiz_external::get_user_best_grade_returns(), $result);
491         $this->assertTrue($result['hasgrade']);
492         $this->assertEquals(8.9, $result['grade']);
494         // Invalid user.
495         try {
496             mod_quiz_external::get_user_best_grade($this->quiz->id, -1);
497             $this->fail('Exception expected due to missing capability.');
498         } catch (dml_missing_record_exception $e) {
499             $this->assertEquals('invaliduser', $e->errorcode);
500         }
502         // Remove the created data.
503         $DB->delete_records('quiz_grades', array('id' => $grade->id));
505     }
506     /**
507      * Test get_combined_review_options.
508      * This is a basic test, this is already tested in mod_quiz_display_options_testcase.
509      */
510     public function test_get_combined_review_options() {
511         global $DB;
513         // Create a new quiz with attempts.
514         $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
515         $data = array('course' => $this->course->id,
516                       'sumgrades' => 1);
517         $quiz = $quizgenerator->create_instance($data);
519         // Create a couple of questions.
520         $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
522         $cat = $questiongenerator->create_question_category();
523         $question = $questiongenerator->create_question('numerical', null, array('category' => $cat->id));
524         quiz_add_quiz_question($question->id, $quiz);
526         $quizobj = quiz::create($quiz->id, $this->student->id);
528         // Set grade to pass.
529         $item = grade_item::fetch(array('courseid' => $this->course->id, 'itemtype' => 'mod',
530                                         'itemmodule' => 'quiz', 'iteminstance' => $quiz->id, 'outcomeid' => null));
531         $item->gradepass = 80;
532         $item->update();
534         // Start the passing attempt.
535         $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
536         $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
538         $timenow = time();
539         $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $this->student->id);
540         quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
541         quiz_attempt_save_started($quizobj, $quba, $attempt);
543         $this->setUser($this->student);
545         $result = mod_quiz_external::get_combined_review_options($quiz->id);
546         $result = external_api::clean_returnvalue(mod_quiz_external::get_combined_review_options_returns(), $result);
548         // Expected values.
549         $expected = array(
550             "someoptions" => array(
551                 array("name" => "feedback", "value" => 1),
552                 array("name" => "generalfeedback", "value" => 1),
553                 array("name" => "rightanswer", "value" => 1),
554                 array("name" => "overallfeedback", "value" => 0),
555                 array("name" => "marks", "value" => 2),
556             ),
557             "alloptions" => array(
558                 array("name" => "feedback", "value" => 1),
559                 array("name" => "generalfeedback", "value" => 1),
560                 array("name" => "rightanswer", "value" => 1),
561                 array("name" => "overallfeedback", "value" => 0),
562                 array("name" => "marks", "value" => 2),
563             ),
564             "warnings" => [],
565         );
567         $this->assertEquals($expected, $result);
569         // Now, finish the attempt.
570         $attemptobj = quiz_attempt::create($attempt->id);
571         $attemptobj->process_finish($timenow, false);
573         $expected = array(
574             "someoptions" => array(
575                 array("name" => "feedback", "value" => 1),
576                 array("name" => "generalfeedback", "value" => 1),
577                 array("name" => "rightanswer", "value" => 1),
578                 array("name" => "overallfeedback", "value" => 1),
579                 array("name" => "marks", "value" => 2),
580             ),
581             "alloptions" => array(
582                 array("name" => "feedback", "value" => 1),
583                 array("name" => "generalfeedback", "value" => 1),
584                 array("name" => "rightanswer", "value" => 1),
585                 array("name" => "overallfeedback", "value" => 1),
586                 array("name" => "marks", "value" => 2),
587             ),
588             "warnings" => [],
589         );
591         // We should see now the overall feedback.
592         $result = mod_quiz_external::get_combined_review_options($quiz->id);
593         $result = external_api::clean_returnvalue(mod_quiz_external::get_combined_review_options_returns(), $result);
594         $this->assertEquals($expected, $result);
596         // Start a new attempt, but not finish it.
597         $timenow = time();
598         $attempt = quiz_create_attempt($quizobj, 2, false, $timenow, false, $this->student->id);
599         $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
600         $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
601         quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
602         quiz_attempt_save_started($quizobj, $quba, $attempt);
604         $expected = array(
605             "someoptions" => array(
606                 array("name" => "feedback", "value" => 1),
607                 array("name" => "generalfeedback", "value" => 1),
608                 array("name" => "rightanswer", "value" => 1),
609                 array("name" => "overallfeedback", "value" => 1),
610                 array("name" => "marks", "value" => 2),
611             ),
612             "alloptions" => array(
613                 array("name" => "feedback", "value" => 1),
614                 array("name" => "generalfeedback", "value" => 1),
615                 array("name" => "rightanswer", "value" => 1),
616                 array("name" => "overallfeedback", "value" => 0),
617                 array("name" => "marks", "value" => 2),
618             ),
619             "warnings" => [],
620         );
622         $result = mod_quiz_external::get_combined_review_options($quiz->id);
623         $result = external_api::clean_returnvalue(mod_quiz_external::get_combined_review_options_returns(), $result);
624         $this->assertEquals($expected, $result);
626         // Teacher, for see student options.
627         $this->setUser($this->teacher);
629         $result = mod_quiz_external::get_combined_review_options($quiz->id, $this->student->id);
630         $result = external_api::clean_returnvalue(mod_quiz_external::get_combined_review_options_returns(), $result);
632         $this->assertEquals($expected, $result);
634         // Invalid user.
635         try {
636             mod_quiz_external::get_combined_review_options($quiz->id, -1);
637             $this->fail('Exception expected due to missing capability.');
638         } catch (dml_missing_record_exception $e) {
639             $this->assertEquals('invaliduser', $e->errorcode);
640         }
641     }
643     /**
644      * Test start_attempt
645      */
646     public function test_start_attempt() {
647         global $DB;
649         // Create a new quiz with questions.
650         list($quiz, $context, $quizobj) = $this->create_quiz_with_questions();
652         $this->setUser($this->student);
654         // Try to open attempt in closed quiz.
655         $quiz->timeopen = time() - WEEKSECS;
656         $quiz->timeclose = time() - DAYSECS;
657         $DB->update_record('quiz', $quiz);
658         $result = mod_quiz_external::start_attempt($quiz->id);
659         $result = external_api::clean_returnvalue(mod_quiz_external::start_attempt_returns(), $result);
661         $this->assertEquals([], $result['attempt']);
662         $this->assertCount(1, $result['warnings']);
664         // Now with a password.
665         $quiz->timeopen = 0;
666         $quiz->timeclose = 0;
667         $quiz->password = 'abc';
668         $DB->update_record('quiz', $quiz);
670         try {
671             mod_quiz_external::start_attempt($quiz->id, array(array("name" => "quizpassword", "value" => 'bad')));
672             $this->fail('Exception expected due to invalid passwod.');
673         } catch (moodle_exception $e) {
674             $this->assertEquals(get_string('passworderror', 'quizaccess_password'), $e->errorcode);
675         }
677         // Now, try everything correct.
678         $result = mod_quiz_external::start_attempt($quiz->id, array(array("name" => "quizpassword", "value" => 'abc')));
679         $result = external_api::clean_returnvalue(mod_quiz_external::start_attempt_returns(), $result);
681         $this->assertEquals(1, $result['attempt']['attempt']);
682         $this->assertEquals($this->student->id, $result['attempt']['userid']);
683         $this->assertEquals($quiz->id, $result['attempt']['quiz']);
684         $this->assertCount(0, $result['warnings']);
685         $attemptid = $result['attempt']['id'];
687         // We are good, try to start a new attempt now.
689         try {
690             mod_quiz_external::start_attempt($quiz->id, array(array("name" => "quizpassword", "value" => 'abc')));
691             $this->fail('Exception expected due to attempt not finished.');
692         } catch (moodle_quiz_exception $e) {
693             $this->assertEquals('attemptstillinprogress', $e->errorcode);
694         }
696         // Finish the started attempt.
698         // Process some responses from the student.
699         $timenow = time();
700         $attemptobj = quiz_attempt::create($attemptid);
701         $tosubmit = array(1 => array('answer' => '3.14'));
702         $attemptobj->process_submitted_actions($timenow, false, $tosubmit);
704         // Finish the attempt.
705         $attemptobj = quiz_attempt::create($attemptid);
706         $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
707         $attemptobj->process_finish($timenow, false);
709         // We should be able to start a new attempt.
710         $result = mod_quiz_external::start_attempt($quiz->id, array(array("name" => "quizpassword", "value" => 'abc')));
711         $result = external_api::clean_returnvalue(mod_quiz_external::start_attempt_returns(), $result);
713         $this->assertEquals(2, $result['attempt']['attempt']);
714         $this->assertEquals($this->student->id, $result['attempt']['userid']);
715         $this->assertEquals($quiz->id, $result['attempt']['quiz']);
716         $this->assertCount(0, $result['warnings']);
718         // Test user with no capabilities.
719         // We need a explicit prohibit since this capability is only defined in authenticated user and guest roles.
720         assign_capability('mod/quiz:attempt', CAP_PROHIBIT, $this->studentrole->id, $context->id);
721         // Empty all the caches that may be affected  by this change.
722         accesslib_clear_all_caches_for_unit_testing();
723         course_modinfo::clear_instance_cache();
725         try {
726             mod_quiz_external::start_attempt($quiz->id);
727             $this->fail('Exception expected due to missing capability.');
728         } catch (required_capability_exception $e) {
729             $this->assertEquals('nopermissions', $e->errorcode);
730         }
732     }
734     /**
735      * Test validate_attempt
736      */
737     public function test_validate_attempt() {
738         global $DB;
740         // Create a new quiz with one attempt started.
741         list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true);
743         $this->setUser($this->student);
745         // Invalid attempt.
746         try {
747             $params = array('attemptid' => -1, 'page' => 0);
748             testable_mod_quiz_external::validate_attempt($params);
749             $this->fail('Exception expected due to invalid attempt id.');
750         } catch (dml_missing_record_exception $e) {
751             $this->assertEquals('invalidrecord', $e->errorcode);
752         }
754         // Test OK case.
755         $params = array('attemptid' => $attempt->id, 'page' => 0);
756         $result = testable_mod_quiz_external::validate_attempt($params);
757         $this->assertEquals($attempt->id, $result[0]->get_attempt()->id);
758         $this->assertEquals([], $result[1]);
760         // Test with preflight data.
761         $quiz->password = 'abc';
762         $DB->update_record('quiz', $quiz);
764         try {
765             $params = array('attemptid' => $attempt->id, 'page' => 0,
766                             'preflightdata' => array(array("name" => "quizpassword", "value" => 'bad')));
767             testable_mod_quiz_external::validate_attempt($params);
768             $this->fail('Exception expected due to invalid passwod.');
769         } catch (moodle_exception $e) {
770             $this->assertEquals(get_string('passworderror', 'quizaccess_password'), $e->errorcode);
771         }
773         // Now, try everything correct.
774         $params['preflightdata'][0]['value'] = 'abc';
775         $result = testable_mod_quiz_external::validate_attempt($params);
776         $this->assertEquals($attempt->id, $result[0]->get_attempt()->id);
777         $this->assertEquals([], $result[1]);
779         // Page out of range.
780         $DB->update_record('quiz', $quiz);
781         $params['page'] = 4;
782         try {
783             testable_mod_quiz_external::validate_attempt($params);
784             $this->fail('Exception expected due to page out of range.');
785         } catch (moodle_quiz_exception $e) {
786             $this->assertEquals('Invalid page number', $e->errorcode);
787         }
789         $params['page'] = 0;
790         // Try to open attempt in closed quiz.
791         $quiz->timeopen = time() - WEEKSECS;
792         $quiz->timeclose = time() - DAYSECS;
793         $DB->update_record('quiz', $quiz);
795         // This should work, ommit access rules.
796         testable_mod_quiz_external::validate_attempt($params, false);
798         // Get a generic error because prior to checking the dates the attempt is closed.
799         try {
800             testable_mod_quiz_external::validate_attempt($params);
801             $this->fail('Exception expected due to passed dates.');
802         } catch (moodle_quiz_exception $e) {
803             $this->assertEquals('attempterror', $e->errorcode);
804         }
806         // Finish the attempt.
807         $attemptobj = quiz_attempt::create($attempt->id);
808         $attemptobj->process_finish(time(), false);
810         try {
811             testable_mod_quiz_external::validate_attempt($params, false);
812             $this->fail('Exception expected due to attempt finished.');
813         } catch (moodle_quiz_exception $e) {
814             $this->assertEquals('attemptalreadyclosed', $e->errorcode);
815         }
817         // Test user with no capabilities.
818         // We need a explicit prohibit since this capability is only defined in authenticated user and guest roles.
819         assign_capability('mod/quiz:attempt', CAP_PROHIBIT, $this->studentrole->id, $context->id);
820         // Empty all the caches that may be affected  by this change.
821         accesslib_clear_all_caches_for_unit_testing();
822         course_modinfo::clear_instance_cache();
824         try {
825             testable_mod_quiz_external::validate_attempt($params);
826             $this->fail('Exception expected due to missing permissions.');
827         } catch (required_capability_exception $e) {
828             $this->assertEquals('nopermissions', $e->errorcode);
829         }
831         // Now try with a different user.
832         $this->setUser($this->teacher);
834         $params['page'] = 0;
835         try {
836             testable_mod_quiz_external::validate_attempt($params);
837             $this->fail('Exception expected due to not your attempt.');
838         } catch (moodle_quiz_exception $e) {
839             $this->assertEquals('notyourattempt', $e->errorcode);
840         }
841     }
843     /**
844      * Test get_attempt_data
845      */
846     public function test_get_attempt_data() {
847         global $DB;
849         // Create a new quiz with one attempt started.
850         list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true);
852         $quizobj = $attemptobj->get_quizobj();
853         $quizobj->preload_questions();
854         $quizobj->load_questions();
855         $questions = $quizobj->get_questions();
857         $this->setUser($this->student);
859         // We receive one question per page.
860         $result = mod_quiz_external::get_attempt_data($attempt->id, 0);
861         $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_data_returns(), $result);
863         $this->assertEquals($attempt, (object) $result['attempt']);
864         $this->assertEquals(1, $result['nextpage']);
865         $this->assertCount(0, $result['messages']);
866         $this->assertCount(1, $result['questions']);
867         $this->assertEquals(1, $result['questions'][0]['slot']);
868         $this->assertEquals(1, $result['questions'][0]['number']);
869         $this->assertEquals('numerical', $result['questions'][0]['type']);
870         $this->assertEquals('todo', $result['questions'][0]['state']);
871         $this->assertEquals(get_string('notyetanswered', 'question'), $result['questions'][0]['status']);
872         $this->assertFalse($result['questions'][0]['flagged']);
873         $this->assertEquals(0, $result['questions'][0]['page']);
874         $this->assertEmpty($result['questions'][0]['mark']);
875         $this->assertEquals(1, $result['questions'][0]['maxmark']);
877         // Now try the last page.
878         $result = mod_quiz_external::get_attempt_data($attempt->id, 1);
879         $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_data_returns(), $result);
881         $this->assertEquals($attempt, (object) $result['attempt']);
882         $this->assertEquals(-1, $result['nextpage']);
883         $this->assertCount(0, $result['messages']);
884         $this->assertCount(1, $result['questions']);
885         $this->assertEquals(2, $result['questions'][0]['slot']);
886         $this->assertEquals(2, $result['questions'][0]['number']);
887         $this->assertEquals('numerical', $result['questions'][0]['type']);
888         $this->assertEquals('todo', $result['questions'][0]['state']);
889         $this->assertEquals(get_string('notyetanswered', 'question'), $result['questions'][0]['status']);
890         $this->assertFalse($result['questions'][0]['flagged']);
891         $this->assertEquals(1, $result['questions'][0]['page']);
893         // Finish previous attempt.
894         $attemptobj->process_finish(time(), false);
896         // Change setting and expect two pages.
897         $quiz->questionsperpage = 4;
898         $DB->update_record('quiz', $quiz);
899         quiz_repaginate_questions($quiz->id, $quiz->questionsperpage);
901         // Start with new attempt with the new layout.
902         $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
903         $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
905         $timenow = time();
906         $attempt = quiz_create_attempt($quizobj, 2, false, $timenow, false, $this->student->id);
907         quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
908         quiz_attempt_save_started($quizobj, $quba, $attempt);
910         // We receive two questions per page.
911         $result = mod_quiz_external::get_attempt_data($attempt->id, 0);
912         $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_data_returns(), $result);
913         $this->assertCount(2, $result['questions']);
914         $this->assertEquals(-1, $result['nextpage']);
916         // Check questions looks good.
917         $found = 0;
918         foreach ($questions as $question) {
919             foreach ($result['questions'] as $rquestion) {
920                 if ($rquestion['slot'] == $question->slot) {
921                     $this->assertTrue(strpos($rquestion['html'], "qid=$question->id") !== false);
922                     $found++;
923                 }
924             }
925         }
926         $this->assertEquals(2, $found);
928     }
930     /**
931      * Test get_attempt_summary
932      */
933     public function test_get_attempt_summary() {
935         // Create a new quiz with one attempt started.
936         list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true);
938         $this->setUser($this->student);
939         $result = mod_quiz_external::get_attempt_summary($attempt->id);
940         $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result);
942         // Check the state, flagged and mark data is correct.
943         $this->assertEquals('todo', $result['questions'][0]['state']);
944         $this->assertEquals('todo', $result['questions'][1]['state']);
945         $this->assertEquals(1, $result['questions'][0]['number']);
946         $this->assertEquals(2, $result['questions'][1]['number']);
947         $this->assertFalse($result['questions'][0]['flagged']);
948         $this->assertFalse($result['questions'][1]['flagged']);
949         $this->assertEmpty($result['questions'][0]['mark']);
950         $this->assertEmpty($result['questions'][1]['mark']);
952         // Submit a response for the first question.
953         $tosubmit = array(1 => array('answer' => '3.14'));
954         $attemptobj->process_submitted_actions(time(), false, $tosubmit);
955         $result = mod_quiz_external::get_attempt_summary($attempt->id);
956         $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result);
958         // Check it's marked as completed only the first one.
959         $this->assertEquals('complete', $result['questions'][0]['state']);
960         $this->assertEquals('todo', $result['questions'][1]['state']);
961         $this->assertEquals(1, $result['questions'][0]['number']);
962         $this->assertEquals(2, $result['questions'][1]['number']);
963         $this->assertFalse($result['questions'][0]['flagged']);
964         $this->assertFalse($result['questions'][1]['flagged']);
965         $this->assertEmpty($result['questions'][0]['mark']);
966         $this->assertEmpty($result['questions'][1]['mark']);
968     }
970     /**
971      * Test save_attempt
972      */
973     public function test_save_attempt() {
975         // Create a new quiz with one attempt started.
976         list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true);
978         // Response for slot 1.
979         $prefix = $quba->get_field_prefix(1);
980         $data = array(
981             array('name' => 'slots', 'value' => 1),
982             array('name' => $prefix . ':sequencecheck',
983                     'value' => $attemptobj->get_question_attempt(1)->get_sequence_check_count()),
984             array('name' => $prefix . 'answer', 'value' => 1),
985         );
987         $this->setUser($this->student);
989         $result = mod_quiz_external::save_attempt($attempt->id, $data);
990         $result = external_api::clean_returnvalue(mod_quiz_external::save_attempt_returns(), $result);
991         $this->assertTrue($result['status']);
993         // Now, get the summary.
994         $result = mod_quiz_external::get_attempt_summary($attempt->id);
995         $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result);
997         // Check it's marked as completed only the first one.
998         $this->assertEquals('complete', $result['questions'][0]['state']);
999         $this->assertEquals('todo', $result['questions'][1]['state']);
1000         $this->assertEquals(1, $result['questions'][0]['number']);
1001         $this->assertEquals(2, $result['questions'][1]['number']);
1002         $this->assertFalse($result['questions'][0]['flagged']);
1003         $this->assertFalse($result['questions'][1]['flagged']);
1004         $this->assertEmpty($result['questions'][0]['mark']);
1005         $this->assertEmpty($result['questions'][1]['mark']);
1007         // Now, second slot.
1008         $prefix = $quba->get_field_prefix(2);
1009         $data = array(
1010             array('name' => 'slots', 'value' => 2),
1011             array('name' => $prefix . ':sequencecheck',
1012                     'value' => $attemptobj->get_question_attempt(1)->get_sequence_check_count()),
1013             array('name' => $prefix . 'answer', 'value' => 1),
1014         );
1016         $result = mod_quiz_external::save_attempt($attempt->id, $data);
1017         $result = external_api::clean_returnvalue(mod_quiz_external::save_attempt_returns(), $result);
1018         $this->assertTrue($result['status']);
1020         // Now, get the summary.
1021         $result = mod_quiz_external::get_attempt_summary($attempt->id);
1022         $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result);
1024         // Check it's marked as completed only the first one.
1025         $this->assertEquals('complete', $result['questions'][0]['state']);
1026         $this->assertEquals('complete', $result['questions'][1]['state']);
1028     }
1030     /**
1031      * Test process_attempt
1032      */
1033     public function test_process_attempt() {
1034         global $DB;
1036         // Create a new quiz with two questions and one attempt started.
1037         list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true);
1039         // Response for slot 1.
1040         $prefix = $quba->get_field_prefix(1);
1041         $data = array(
1042             array('name' => 'slots', 'value' => 1),
1043             array('name' => $prefix . ':sequencecheck',
1044                     'value' => $attemptobj->get_question_attempt(1)->get_sequence_check_count()),
1045             array('name' => $prefix . 'answer', 'value' => 1),
1046         );
1048         $this->setUser($this->student);
1050         $result = mod_quiz_external::process_attempt($attempt->id, $data);
1051         $result = external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result);
1052         $this->assertEquals(quiz_attempt::IN_PROGRESS, $result['state']);
1054         // Now, get the summary.
1055         $result = mod_quiz_external::get_attempt_summary($attempt->id);
1056         $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result);
1058         // Check it's marked as completed only the first one.
1059         $this->assertEquals('complete', $result['questions'][0]['state']);
1060         $this->assertEquals('todo', $result['questions'][1]['state']);
1061         $this->assertEquals(1, $result['questions'][0]['number']);
1062         $this->assertEquals(2, $result['questions'][1]['number']);
1063         $this->assertFalse($result['questions'][0]['flagged']);
1064         $this->assertFalse($result['questions'][1]['flagged']);
1065         $this->assertEmpty($result['questions'][0]['mark']);
1066         $this->assertEmpty($result['questions'][1]['mark']);
1068         // Now, second slot.
1069         $prefix = $quba->get_field_prefix(2);
1070         $data = array(
1071             array('name' => 'slots', 'value' => 2),
1072             array('name' => $prefix . ':sequencecheck',
1073                     'value' => $attemptobj->get_question_attempt(1)->get_sequence_check_count()),
1074             array('name' => $prefix . 'answer', 'value' => 1),
1075             array('name' => $prefix . ':flagged', 'value' => 1),
1076         );
1078         $result = mod_quiz_external::process_attempt($attempt->id, $data);
1079         $result = external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result);
1080         $this->assertEquals(quiz_attempt::IN_PROGRESS, $result['state']);
1082         // Now, get the summary.
1083         $result = mod_quiz_external::get_attempt_summary($attempt->id);
1084         $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result);
1086         // Check it's marked as completed only the first one.
1087         $this->assertEquals('complete', $result['questions'][0]['state']);
1088         $this->assertEquals('complete', $result['questions'][1]['state']);
1089         $this->assertFalse($result['questions'][0]['flagged']);
1090         $this->assertTrue($result['questions'][1]['flagged']);
1092         // Finish the attempt.
1093         $result = mod_quiz_external::process_attempt($attempt->id, array(), true);
1094         $result = external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result);
1095         $this->assertEquals(quiz_attempt::FINISHED, $result['state']);
1097         // Start new attempt.
1098         $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
1099         $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
1101         $timenow = time();
1102         $attempt = quiz_create_attempt($quizobj, 2, false, $timenow, false, $this->student->id);
1103         quiz_start_new_attempt($quizobj, $quba, $attempt, 2, $timenow);
1104         quiz_attempt_save_started($quizobj, $quba, $attempt);
1106         // Force grace period, attempt going to overdue.
1107         $quiz->timeclose = $timenow - 10;
1108         $quiz->graceperiod = 60;
1109         $quiz->overduehandling = 'graceperiod';
1110         $DB->update_record('quiz', $quiz);
1112         $result = mod_quiz_external::process_attempt($attempt->id, array());
1113         $result = external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result);
1114         $this->assertEquals(quiz_attempt::OVERDUE, $result['state']);
1116         // New attempt.
1117         $timenow = time();
1118         $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
1119         $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
1120         $attempt = quiz_create_attempt($quizobj, 3, 2, $timenow, false, $this->student->id);
1121         quiz_start_new_attempt($quizobj, $quba, $attempt, 3, $timenow);
1122         quiz_attempt_save_started($quizobj, $quba, $attempt);
1124         // Force abandon.
1125         $quiz->timeclose = $timenow - HOURSECS;
1126         $DB->update_record('quiz', $quiz);
1128         $result = mod_quiz_external::process_attempt($attempt->id, array());
1129         $result = external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result);
1130         $this->assertEquals(quiz_attempt::ABANDONED, $result['state']);
1132     }
1134     /**
1135      * Test validate_attempt_review
1136      */
1137     public function test_validate_attempt_review() {
1138         global $DB;
1140         // Create a new quiz with one attempt started.
1141         list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true);
1143         $this->setUser($this->student);
1145         // Invalid attempt, invalid id.
1146         try {
1147             $params = array('attemptid' => -1);
1148             testable_mod_quiz_external::validate_attempt_review($params);
1149             $this->fail('Exception expected due invalid id.');
1150         } catch (dml_missing_record_exception $e) {
1151             $this->assertEquals('invalidrecord', $e->errorcode);
1152         }
1154         // Invalid attempt, not closed.
1155         try {
1156             $params = array('attemptid' => $attempt->id);
1157             testable_mod_quiz_external::validate_attempt_review($params);
1158             $this->fail('Exception expected due not closed attempt.');
1159         } catch (moodle_quiz_exception $e) {
1160             $this->assertEquals('attemptclosed', $e->errorcode);
1161         }
1163         // Test ok case (finished attempt).
1164         list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true, true);
1166         $params = array('attemptid' => $attempt->id);
1167         testable_mod_quiz_external::validate_attempt_review($params);
1169         // Teacher should be able to view the review of one student's attempt.
1170         $this->setUser($this->teacher);
1171         testable_mod_quiz_external::validate_attempt_review($params);
1173         // We should not see other students attempts.
1174         $anotherstudent = self::getDataGenerator()->create_user();
1175         $this->getDataGenerator()->enrol_user($anotherstudent->id, $this->course->id, $this->studentrole->id, 'manual');
1177         $this->setUser($anotherstudent);
1178         try {
1179             $params = array('attemptid' => $attempt->id);
1180             testable_mod_quiz_external::validate_attempt_review($params);
1181             $this->fail('Exception expected due missing permissions.');
1182         } catch (moodle_quiz_exception $e) {
1183             $this->assertEquals('noreviewattempt', $e->errorcode);
1184         }
1185     }
1188     /**
1189      * Test get_attempt_review
1190      */
1191     public function test_get_attempt_review() {
1192         global $DB;
1194         // Create a new quiz with two questions and one attempt finished.
1195         list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true, true);
1197         // Add feedback to the quiz.
1198         $feedback = new stdClass();
1199         $feedback->quizid = $quiz->id;
1200         $feedback->feedbacktext = 'Feedback text 1';
1201         $feedback->feedbacktextformat = 1;
1202         $feedback->mingrade = 49;
1203         $feedback->maxgrade = 100;
1204         $feedback->id = $DB->insert_record('quiz_feedback', $feedback);
1206         $feedback->feedbacktext = 'Feedback text 2';
1207         $feedback->feedbacktextformat = 1;
1208         $feedback->mingrade = 30;
1209         $feedback->maxgrade = 48;
1210         $feedback->id = $DB->insert_record('quiz_feedback', $feedback);
1212         $result = mod_quiz_external::get_attempt_review($attempt->id);
1213         $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_review_returns(), $result);
1215         // Two questions, one completed and correct, the other gave up.
1216         $this->assertEquals(50, $result['grade']);
1217         $this->assertEquals(1, $result['attempt']['attempt']);
1218         $this->assertEquals('finished', $result['attempt']['state']);
1219         $this->assertEquals(1, $result['attempt']['sumgrades']);
1220         $this->assertCount(2, $result['questions']);
1221         $this->assertEquals('gradedright', $result['questions'][0]['state']);
1222         $this->assertEquals(1, $result['questions'][0]['slot']);
1223         $this->assertEquals('gaveup', $result['questions'][1]['state']);
1224         $this->assertEquals(2, $result['questions'][1]['slot']);
1226         $this->assertCount(1, $result['additionaldata']);
1227         $this->assertEquals('feedback', $result['additionaldata'][0]['id']);
1228         $this->assertEquals('Feedback', $result['additionaldata'][0]['title']);
1229         $this->assertEquals('Feedback text 1', $result['additionaldata'][0]['content']);
1231         // Only first page.
1232         $result = mod_quiz_external::get_attempt_review($attempt->id, 0);
1233         $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_review_returns(), $result);
1235         $this->assertEquals(50, $result['grade']);
1236         $this->assertEquals(1, $result['attempt']['attempt']);
1237         $this->assertEquals('finished', $result['attempt']['state']);
1238         $this->assertEquals(1, $result['attempt']['sumgrades']);
1239         $this->assertCount(1, $result['questions']);
1240         $this->assertEquals('gradedright', $result['questions'][0]['state']);
1241         $this->assertEquals(1, $result['questions'][0]['slot']);
1243          $this->assertCount(1, $result['additionaldata']);
1244         $this->assertEquals('feedback', $result['additionaldata'][0]['id']);
1245         $this->assertEquals('Feedback', $result['additionaldata'][0]['title']);
1246         $this->assertEquals('Feedback text 1', $result['additionaldata'][0]['content']);
1248     }
1250     /**
1251      * Test test_view_attempt
1252      */
1253     public function test_view_attempt() {
1254         global $DB;
1256         // Create a new quiz with two questions and one attempt started.
1257         list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true, false);
1259         // Test user with full capabilities.
1260         $this->setUser($this->student);
1262         // Trigger and capture the event.
1263         $sink = $this->redirectEvents();
1265         $result = mod_quiz_external::view_attempt($attempt->id, 0);
1266         $result = external_api::clean_returnvalue(mod_quiz_external::view_attempt_returns(), $result);
1267         $this->assertTrue($result['status']);
1269         $events = $sink->get_events();
1270         $this->assertCount(1, $events);
1271         $event = array_shift($events);
1273         // Checking that the event contains the expected values.
1274         $this->assertInstanceOf('\mod_quiz\event\attempt_viewed', $event);
1275         $this->assertEquals($context, $event->get_context());
1276         $this->assertEventContextNotUsed($event);
1277         $this->assertNotEmpty($event->get_name());
1279         // Now, force the quiz with QUIZ_NAVMETHOD_SEQ (sequencial) navigation method.
1280         $DB->set_field('quiz', 'navmethod', QUIZ_NAVMETHOD_SEQ, array('id' => $quiz->id));
1281         // See next page.
1282         $result = mod_quiz_external::view_attempt($attempt->id, 1);
1283         $result = external_api::clean_returnvalue(mod_quiz_external::view_attempt_returns(), $result);
1284         $this->assertTrue($result['status']);
1286         $events = $sink->get_events();
1287         $this->assertCount(2, $events);
1289         // Try to go to previous page.
1290         try {
1291             mod_quiz_external::view_attempt($attempt->id, 0);
1292             $this->fail('Exception expected due to try to see a previous page.');
1293         } catch (moodle_quiz_exception $e) {
1294             $this->assertEquals('Out of sequence access', $e->errorcode);
1295         }
1297     }
1299     /**
1300      * Test test_view_attempt_summary
1301      */
1302     public function test_view_attempt_summary() {
1303         global $DB;
1305         // Create a new quiz with two questions and one attempt started.
1306         list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true, false);
1308         // Test user with full capabilities.
1309         $this->setUser($this->student);
1311         // Trigger and capture the event.
1312         $sink = $this->redirectEvents();
1314         $result = mod_quiz_external::view_attempt_summary($attempt->id, 0);
1315         $result = external_api::clean_returnvalue(mod_quiz_external::view_attempt_summary_returns(), $result);
1316         $this->assertTrue($result['status']);
1318         $events = $sink->get_events();
1319         $this->assertCount(1, $events);
1320         $event = array_shift($events);
1322         // Checking that the event contains the expected values.
1323         $this->assertInstanceOf('\mod_quiz\event\attempt_summary_viewed', $event);
1324         $this->assertEquals($context, $event->get_context());
1325         $moodlequiz = new \moodle_url('/mod/quiz/summary.php', array('attempt' => $attempt->id));
1326         $this->assertEquals($moodlequiz, $event->get_url());
1327         $this->assertEventContextNotUsed($event);
1328         $this->assertNotEmpty($event->get_name());
1330     }
1332     /**
1333      * Test test_view_attempt_summary
1334      */
1335     public function test_view_attempt_review() {
1336         global $DB;
1338         // Create a new quiz with two questions and one attempt finished.
1339         list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true, true);
1341         // Test user with full capabilities.
1342         $this->setUser($this->student);
1344         // Trigger and capture the event.
1345         $sink = $this->redirectEvents();
1347         $result = mod_quiz_external::view_attempt_review($attempt->id, 0);
1348         $result = external_api::clean_returnvalue(mod_quiz_external::view_attempt_review_returns(), $result);
1349         $this->assertTrue($result['status']);
1351         $events = $sink->get_events();
1352         $this->assertCount(1, $events);
1353         $event = array_shift($events);
1355         // Checking that the event contains the expected values.
1356         $this->assertInstanceOf('\mod_quiz\event\attempt_reviewed', $event);
1357         $this->assertEquals($context, $event->get_context());
1358         $moodlequiz = new \moodle_url('/mod/quiz/review.php', array('attempt' => $attempt->id));
1359         $this->assertEquals($moodlequiz, $event->get_url());
1360         $this->assertEventContextNotUsed($event);
1361         $this->assertNotEmpty($event->get_name());
1363     }