Merge branch 'MDL-53034-master' of git://github.com/jleyva/moodle
[moodle.git] / mod / quiz / tests / external_test.php
CommitLineData
51e27aac
JL
1<?php
2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16
17/**
18 * Quiz 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
27defined('MOODLE_INTERNAL') || die();
28
29global $CFG;
30
31require_once($CFG->dirroot . '/webservice/tests/helpers.php');
32
4c08658a
JL
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 */
41class testable_mod_quiz_external extends mod_quiz_external {
42
43 /**
44 * Public accessor.
45 *
46 * @param array $params Array of parameters including the attemptid and preflight data
98e68690
JL
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
4c08658a
JL
49 * @return array containing the attempt object and access messages
50 */
98e68690
JL
51 public static function validate_attempt($params, $checkaccessrules = true, $failifoverdue = true) {
52 return parent::validate_attempt($params, $checkaccessrules, $failifoverdue);
4c08658a 53 }
3589b659
JL
54
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 }
4c08658a
JL
64}
65
51e27aac
JL
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 */
75class mod_quiz_external_testcase extends externallib_advanced_testcase {
76
77 /**
78 * Set up for every test
79 */
80 public function setUp() {
81 global $DB;
82 $this->resetAfterTest();
83 $this->setAdminUser();
84
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);
90
91 // Create users.
92 $this->student = self::getDataGenerator()->create_user();
93 $this->teacher = self::getDataGenerator()->create_user();
94
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 }
101
4c08658a
JL
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) {
110
111 // Create a new quiz with attempts.
112 $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
113 $data = array('course' => $this->course->id,
3589b659 114 'sumgrades' => 2);
4c08658a
JL
115 $quiz = $quizgenerator->create_instance($data);
116 $context = context_module::instance($quiz->cmid);
117
118 // Create a couple of questions.
119 $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
120
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);
126
127 $quizobj = quiz::create($quiz->id, $this->student->id);
128
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();
134
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);
139
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);
145
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);
150
151 // Finish the attempt.
152 $attemptobj->process_finish(time(), false);
153 }
96d5607c 154 return array($quiz, $context, $quizobj, $attempt, $attemptobj, $quba);
4c08658a
JL
155 } else {
156 return array($quiz, $context, $quizobj);
157 }
158
159 }
160
51e27aac
JL
161 /*
162 * Test get quizzes by courses
163 */
164 public function test_mod_quiz_get_quizzes_by_courses() {
165 global $DB;
166
167 // Create additional course.
168 $course2 = self::getDataGenerator()->create_course();
169
170 // Second quiz.
171 $record = new stdClass();
172 $record->course = $course2->id;
173 $quiz2 = self::getDataGenerator()->create_module('quiz', $record);
174
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);
185
186 self::setUser($this->student);
187
188 $returndescription = mod_quiz_external::get_quizzes_by_courses_returns();
189
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');
202
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');
214
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');
224
225 foreach (array_merge($allusersfields, $userswithaccessfields) as $field) {
226 $expected1[$field] = $quiz1->{$field};
227 $expected2[$field] = $quiz2->{$field};
228 }
229
230 $expectedquizzes = array($expected2, $expected1);
231
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);
235
236 $this->assertEquals($expectedquizzes, $result['quizzes']);
237 $this->assertCount(0, $result['warnings']);
238
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']);
244
245 // Unenrol user from second course and alter expected quizzes.
246 $enrol->unenrol_user($instance2, $this->student->id);
247 array_shift($expectedquizzes);
248
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']);
253
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']);
259
260 // Now, try as a teacher for getting all the additional fields.
261 self::setUser($this->teacher);
262
263 foreach ($managerfields as $field) {
264 $expectedquizzes[0][$field] = $quiz1->{$field};
265 }
266
267 $result = mod_quiz_external::get_quizzes_by_courses();
268 $result = external_api::clean_returnvalue($returndescription, $result);
269 $this->assertEquals($expectedquizzes, $result['quizzes']);
270
271 // Admin also should get all the information.
272 self::setAdminUser();
273
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']);
277
278 // Now, prevent access.
279 $enrol->enrol_user($instance2, $this->student->id);
280
281 self::setUser($this->student);
282
283 $quiz2->timeclose = time() - DAYSECS;
284 $DB->update_record('quiz', $quiz2);
285
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']);
296
297 $this->assertFalse(isset($result['quizzes'][0]['timelimit']));
298
299 }
300
4064dd0e
JL
301 /**
302 * Test test_view_quiz
303 */
304 public function test_view_quiz() {
305 global $DB;
306
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 }
314
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 }
324
325 // Test user with full capabilities.
326 $this->setUser($this->student);
327
328 // Trigger and capture the event.
329 $sink = $this->redirectEvents();
330
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']);
334
335 $events = $sink->get_events();
336 $this->assertCount(1, $events);
337 $event = array_shift($events);
338
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());
346
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();
353
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 }
360
361 }
362
c161ecff
JL
363 /**
364 * Test get_user_attempts
365 */
366 public function test_get_user_attempts() {
367
4c08658a
JL
368 // Create a quiz with one attempt finished.
369 list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true, true);
c161ecff
JL
370
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);
374
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']);
380
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);
384
385 $this->assertCount(1, $result['attempts']);
386 $this->assertEquals($attempt->id, $result['attempts'][0]['id']);
387
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);
391
392 $this->assertCount(1, $result['attempts']);
393 $this->assertEquals($attempt->id, $result['attempts'][0]['id']);
394
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);
398
399 $this->assertCount(0, $result['attempts']);
400
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);
406
407 quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
408 quiz_attempt_save_started($quizobj, $quba, $attempt);
409
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);
413
414 $this->assertCount(2, $result['attempts']);
415
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);
419
420 $this->assertCount(1, $result['attempts']);
421
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);
426
427 $this->assertCount(1, $result['attempts']);
428 $this->assertEquals($this->student->id, $result['attempts'][0]['userid']);
429
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);
432
433 $this->assertCount(2, $result['attempts']);
434 $this->assertEquals($this->student->id, $result['attempts'][0]['userid']);
435
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 }
e73e4581
JL
444
445 /**
446 * Test get_user_best_grade
447 */
448 public function test_get_user_best_grade() {
449 global $DB;
450
451 $this->setUser($this->student);
452
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);
455
456 // No grades yet.
457 $this->assertFalse($result['hasgrade']);
458 $this->assertTrue(!isset($result['grade']));
459
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);
466
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);
469
470 // Now I have grades.
471 $this->assertTrue($result['hasgrade']);
472 $this->assertEquals(8.9, $result['grade']);
473
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');
477
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 }
484
485 // Teacher must be able to see student grades.
486 $this->setUser($this->teacher);
487
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);
490
491 $this->assertTrue($result['hasgrade']);
492 $this->assertEquals(8.9, $result['grade']);
493
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 }
501
502 // Remove the created data.
503 $DB->delete_records('quiz_grades', array('id' => $grade->id));
504
505 }
1f67c0b8
JL
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;
512
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);
518
519 // Create a couple of questions.
520 $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
521
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);
525
526 $quizobj = quiz::create($quiz->id, $this->student->id);
527
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();
533
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);
537
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);
542
543 $this->setUser($this->student);
544
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);
547
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 );
566
567 $this->assertEquals($expected, $result);
568
569 // Now, finish the attempt.
570 $attemptobj = quiz_attempt::create($attempt->id);
571 $attemptobj->process_finish($timenow, false);
572
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 );
590
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);
595
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);
603
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 );
621
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);
625
626 // Teacher, for see student options.
627 $this->setUser($this->teacher);
628
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);
631
632 $this->assertEquals($expected, $result);
633
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 }
642
b8954440
JL
643 /**
644 * Test start_attempt
645 */
646 public function test_start_attempt() {
647 global $DB;
648
4c08658a
JL
649 // Create a new quiz with questions.
650 list($quiz, $context, $quizobj) = $this->create_quiz_with_questions();
b8954440
JL
651
652 $this->setUser($this->student);
653
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);
660
661 $this->assertEquals([], $result['attempt']);
662 $this->assertCount(1, $result['warnings']);
663
664 // Now with a password.
665 $quiz->timeopen = 0;
666 $quiz->timeclose = 0;
667 $quiz->password = 'abc';
668 $DB->update_record('quiz', $quiz);
669
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 }
676
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);
680
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'];
686
687 // We are good, try to start a new attempt now.
688
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 }
695
696 // Finish the started attempt.
697
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);
703
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);
708
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);
712
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']);
717
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();
724
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 }
731
732 }
733
bc247b0d 734 /**
4c08658a 735 * Test validate_attempt
bc247b0d 736 */
4c08658a
JL
737 public function test_validate_attempt() {
738 global $DB;
bc247b0d 739
4c08658a
JL
740 // Create a new quiz with one attempt started.
741 list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true);
bc247b0d 742
4c08658a
JL
743 $this->setUser($this->student);
744
745 // Invalid attempt.
bc247b0d 746 try {
4c08658a
JL
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 }
753
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]);
759
760 // Test with preflight data.
761 $quiz->password = 'abc';
762 $DB->update_record('quiz', $quiz);
763
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 }
772
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]);
778
bc9733e7 779 // Page out of range.
4c08658a 780 $DB->update_record('quiz', $quiz);
bc9733e7 781 $params['page'] = 4;
4c08658a
JL
782 try {
783 testable_mod_quiz_external::validate_attempt($params);
bc9733e7 784 $this->fail('Exception expected due to page out of range.');
4c08658a 785 } catch (moodle_quiz_exception $e) {
bc9733e7 786 $this->assertEquals('Invalid page number', $e->errorcode);
4c08658a
JL
787 }
788
bc9733e7
JL
789 $params['page'] = 0;
790 // Try to open attempt in closed quiz.
791 $quiz->timeopen = time() - WEEKSECS;
792 $quiz->timeclose = time() - DAYSECS;
4c08658a 793 $DB->update_record('quiz', $quiz);
98e68690
JL
794
795 // This should work, ommit access rules.
796 testable_mod_quiz_external::validate_attempt($params, false);
797
798 // Get a generic error because prior to checking the dates the attempt is closed.
4c08658a
JL
799 try {
800 testable_mod_quiz_external::validate_attempt($params);
bc9733e7 801 $this->fail('Exception expected due to passed dates.');
4c08658a 802 } catch (moodle_quiz_exception $e) {
98e68690 803 $this->assertEquals('attempterror', $e->errorcode);
4c08658a
JL
804 }
805
806 // Finish the attempt.
807 $attemptobj = quiz_attempt::create($attempt->id);
808 $attemptobj->process_finish(time(), false);
809
810 try {
98e68690 811 testable_mod_quiz_external::validate_attempt($params, false);
4c08658a
JL
812 $this->fail('Exception expected due to attempt finished.');
813 } catch (moodle_quiz_exception $e) {
814 $this->assertEquals('attemptalreadyclosed', $e->errorcode);
815 }
816
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();
823
824 try {
825 testable_mod_quiz_external::validate_attempt($params);
826 $this->fail('Exception expected due to missing permissions.');
bc247b0d
JL
827 } catch (required_capability_exception $e) {
828 $this->assertEquals('nopermissions', $e->errorcode);
829 }
830
4c08658a
JL
831 // Now try with a different user.
832 $this->setUser($this->teacher);
833
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 }
842
843 /**
844 * Test get_attempt_data
845 */
846 public function test_get_attempt_data() {
847 global $DB;
848
849 // Create a new quiz with one attempt started.
850 list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true);
851
852 $quizobj = $attemptobj->get_quizobj();
853 $quizobj->preload_questions();
854 $quizobj->load_questions();
855 $questions = $quizobj->get_questions();
856
857 $this->setUser($this->student);
858
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);
862
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']);
876
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);
880
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']);
892
893 // Finish previous attempt.
894 $attemptobj->process_finish(time(), false);
895
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);
900
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);
904
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);
909
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']);
915
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);
927
bc247b0d
JL
928 }
929
bc9733e7
JL
930 /**
931 * Test get_attempt_summary
932 */
933 public function test_get_attempt_summary() {
934
935 // Create a new quiz with one attempt started.
936 list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true);
937
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);
941
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']);
951
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);
957
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']);
967
968 }
969
96d5607c
JL
970 /**
971 * Test save_attempt
972 */
973 public function test_save_attempt() {
974
975 // Create a new quiz with one attempt started.
976 list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true);
977
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 );
986
987 $this->setUser($this->student);
988
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']);
992
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);
996
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']);
1006
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 );
1015
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']);
1019
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);
1023
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']);
1027
1028 }
1029
98e68690
JL
1030 /**
1031 * Test process_attempt
1032 */
1033 public function test_process_attempt() {
1034 global $DB;
1035
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);
1038
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 );
1047
1048 $this->setUser($this->student);
1049
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']);
1053
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);
1057
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']);
1067
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 );
1077
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']);
1081
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);
1085
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']);
1091
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']);
1096
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);
1100
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);
1105
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);
1111
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']);
1115
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);
1123
1124 // Force abandon.
1125 $quiz->timeclose = $timenow - HOURSECS;
1126 $DB->update_record('quiz', $quiz);
1127
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']);
1131
1132 }
1133
3589b659
JL
1134 /**
1135 * Test validate_attempt_review
1136 */
1137 public function test_validate_attempt_review() {
1138 global $DB;
1139
1140 // Create a new quiz with one attempt started.
1141 list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true);
1142
1143 $this->setUser($this->student);
1144
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 }
1153
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 }
1162
1163 // Test ok case (finished attempt).
1164 list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true, true);
1165
1166 $params = array('attemptid' => $attempt->id);
1167 testable_mod_quiz_external::validate_attempt_review($params);
1168
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);
1172
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');
1176
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 }
1186
1187
1188 /**
1189 * Test get_attempt_review
1190 */
1191 public function test_get_attempt_review() {
1192 global $DB;
1193
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);
1196
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);
1205
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);
1211
1212 $result = mod_quiz_external::get_attempt_review($attempt->id);
1213 $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_review_returns(), $result);
1214
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']);
1225
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']);
1230
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);
1234
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']);
1242
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']);
1247
1248 }
1249
899983ee
JL
1250 /**
1251 * Test test_view_attempt
1252 */
1253 public function test_view_attempt() {
1254 global $DB;
1255
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);
1258
1259 // Test user with full capabilities.
1260 $this->setUser($this->student);
1261
1262 // Trigger and capture the event.
1263 $sink = $this->redirectEvents();
1264
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']);
1268
1269 $events = $sink->get_events();
1270 $this->assertCount(1, $events);
1271 $event = array_shift($events);
1272
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());
1278
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']);
1285
1286 $events = $sink->get_events();
1287 $this->assertCount(2, $events);
1288
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 }
1296
1297 }
1298
d9ef6ae0
JL
1299 /**
1300 * Test test_view_attempt_summary
1301 */
1302 public function test_view_attempt_summary() {
1303 global $DB;
1304
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);
1307
1308 // Test user with full capabilities.
1309 $this->setUser($this->student);
1310
1311 // Trigger and capture the event.
1312 $sink = $this->redirectEvents();
1313
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']);
1317
1318 $events = $sink->get_events();
1319 $this->assertCount(1, $events);
1320 $event = array_shift($events);
1321
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());
1329
1330 }
1331
3e5c19a0
JL
1332 /**
1333 * Test test_view_attempt_summary
1334 */
1335 public function test_view_attempt_review() {
1336 global $DB;
1337
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);
1340
1341 // Test user with full capabilities.
1342 $this->setUser($this->student);
1343
1344 // Trigger and capture the event.
1345 $sink = $this->redirectEvents();
1346
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']);
1350
1351 $events = $sink->get_events();
1352 $this->assertCount(1, $events);
1353 $event = array_shift($events);
1354
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());
1362
1363 }
1364
48abca79
JL
1365 /**
1366 * Test get_quiz_feedback_for_grade
1367 */
1368 public function test_get_quiz_feedback_for_grade() {
1369 global $DB;
1370
1371 // Add feedback to the quiz.
1372 $feedback = new stdClass();
1373 $feedback->quizid = $this->quiz->id;
1374 $feedback->feedbacktext = 'Feedback text 1';
1375 $feedback->feedbacktextformat = 1;
1376 $feedback->mingrade = 49;
1377 $feedback->maxgrade = 100;
1378 $feedback->id = $DB->insert_record('quiz_feedback', $feedback);
1379
1380 $feedback->feedbacktext = 'Feedback text 2';
1381 $feedback->feedbacktextformat = 1;
1382 $feedback->mingrade = 30;
1383 $feedback->maxgrade = 49;
1384 $feedback->id = $DB->insert_record('quiz_feedback', $feedback);
1385
1386 $result = mod_quiz_external::get_quiz_feedback_for_grade($this->quiz->id, 50);
1387 $result = external_api::clean_returnvalue(mod_quiz_external::get_quiz_feedback_for_grade_returns(), $result);
1388 $this->assertEquals('Feedback text 1', $result['feedbacktext']);
1389 $this->assertEquals(FORMAT_HTML, $result['feedbacktextformat']);
1390
1391 $result = mod_quiz_external::get_quiz_feedback_for_grade($this->quiz->id, 30);
1392 $result = external_api::clean_returnvalue(mod_quiz_external::get_quiz_feedback_for_grade_returns(), $result);
1393 $this->assertEquals('Feedback text 2', $result['feedbacktext']);
1394 $this->assertEquals(FORMAT_HTML, $result['feedbacktextformat']);
1395
1396 $result = mod_quiz_external::get_quiz_feedback_for_grade($this->quiz->id, 10);
1397 $result = external_api::clean_returnvalue(mod_quiz_external::get_quiz_feedback_for_grade_returns(), $result);
1398 $this->assertEquals('', $result['feedbacktext']);
1399 $this->assertEquals(FORMAT_MOODLE, $result['feedbacktextformat']);
1400 }
1401
51e27aac 1402}