Differences Between: [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * Quiz module external functions tests. 19 * 20 * @package mod_quiz 21 * @category external 22 * @copyright 2016 Juan Leyva <juan@moodle.com> 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 * @since Moodle 3.1 25 */ 26 27 namespace mod_quiz\external; 28 29 use externallib_advanced_testcase; 30 use mod_quiz_external; 31 use mod_quiz_display_options; 32 use quiz; 33 use quiz_attempt; 34 35 defined('MOODLE_INTERNAL') || die(); 36 37 global $CFG; 38 39 require_once($CFG->dirroot . '/webservice/tests/helpers.php'); 40 41 /** 42 * Silly class to access mod_quiz_external internal methods. 43 * 44 * @package mod_quiz 45 * @copyright 2016 Juan Leyva <juan@moodle.com> 46 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 47 * @since Moodle 3.1 48 */ 49 class testable_mod_quiz_external extends mod_quiz_external { 50 51 /** 52 * Public accessor. 53 * 54 * @param array $params Array of parameters including the attemptid and preflight data 55 * @param bool $checkaccessrules whether to check the quiz access rules or not 56 * @param bool $failifoverdue whether to return error if the attempt is overdue 57 * @return array containing the attempt object and access messages 58 */ 59 public static function validate_attempt($params, $checkaccessrules = true, $failifoverdue = true) { 60 return parent::validate_attempt($params, $checkaccessrules, $failifoverdue); 61 } 62 63 /** 64 * Public accessor. 65 * 66 * @param array $params Array of parameters including the attemptid 67 * @return array containing the attempt object and display options 68 */ 69 public static function validate_attempt_review($params) { 70 return parent::validate_attempt_review($params); 71 } 72 } 73 74 /** 75 * Quiz module external functions tests 76 * 77 * @package mod_quiz 78 * @category external 79 * @copyright 2016 Juan Leyva <juan@moodle.com> 80 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 81 * @since Moodle 3.1 82 */ 83 class external_test extends externallib_advanced_testcase { 84 85 /** 86 * Set up for every test 87 */ 88 public function setUp(): void { 89 global $DB; 90 $this->resetAfterTest(); 91 $this->setAdminUser(); 92 93 // Setup test data. 94 $this->course = $this->getDataGenerator()->create_course(); 95 $this->quiz = $this->getDataGenerator()->create_module('quiz', array('course' => $this->course->id)); 96 $this->context = \context_module::instance($this->quiz->cmid); 97 $this->cm = get_coursemodule_from_instance('quiz', $this->quiz->id); 98 99 // Create users. 100 $this->student = self::getDataGenerator()->create_user(); 101 $this->teacher = self::getDataGenerator()->create_user(); 102 103 // Users enrolments. 104 $this->studentrole = $DB->get_record('role', array('shortname' => 'student')); 105 $this->teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher')); 106 // Allow student to receive messages. 107 $coursecontext = \context_course::instance($this->course->id); 108 assign_capability('mod/quiz:emailnotifysubmission', CAP_ALLOW, $this->teacherrole->id, $coursecontext, true); 109 110 $this->getDataGenerator()->enrol_user($this->student->id, $this->course->id, $this->studentrole->id, 'manual'); 111 $this->getDataGenerator()->enrol_user($this->teacher->id, $this->course->id, $this->teacherrole->id, 'manual'); 112 } 113 114 /** 115 * Create a quiz with questions including a started or finished attempt optionally 116 * 117 * @param boolean $startattempt whether to start a new attempt 118 * @param boolean $finishattempt whether to finish the new attempt 119 * @param string $behaviour the quiz preferredbehaviour, defaults to 'deferredfeedback'. 120 * @param boolean $includeqattachments whether to include a question that supports attachments, defaults to false. 121 * @param array $extraoptions extra options for Quiz. 122 * @return array array containing the quiz, context and the attempt 123 */ 124 private function create_quiz_with_questions($startattempt = false, $finishattempt = false, $behaviour = 'deferredfeedback', 125 $includeqattachments = false, $extraoptions = []) { 126 127 // Create a new quiz with attempts. 128 $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); 129 $data = array('course' => $this->course->id, 130 'sumgrades' => 2, 131 'preferredbehaviour' => $behaviour); 132 $data = array_merge($data, $extraoptions); 133 $quiz = $quizgenerator->create_instance($data); 134 $context = \context_module::instance($quiz->cmid); 135 136 // Create a couple of questions. 137 $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); 138 139 $cat = $questiongenerator->create_question_category(); 140 $question = $questiongenerator->create_question('numerical', null, array('category' => $cat->id)); 141 quiz_add_quiz_question($question->id, $quiz); 142 $question = $questiongenerator->create_question('numerical', null, array('category' => $cat->id)); 143 quiz_add_quiz_question($question->id, $quiz); 144 145 if ($includeqattachments) { 146 $question = $questiongenerator->create_question('essay', null, array('category' => $cat->id, 'attachments' => 1, 147 'attachmentsrequired' => 1)); 148 quiz_add_quiz_question($question->id, $quiz); 149 } 150 151 $quizobj = quiz::create($quiz->id, $this->student->id); 152 153 // Set grade to pass. 154 $item = \grade_item::fetch(array('courseid' => $this->course->id, 'itemtype' => 'mod', 155 'itemmodule' => 'quiz', 'iteminstance' => $quiz->id, 'outcomeid' => null)); 156 $item->gradepass = 80; 157 $item->update(); 158 159 if ($startattempt or $finishattempt) { 160 // Now, do one attempt. 161 $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); 162 $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); 163 164 $timenow = time(); 165 $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $this->student->id); 166 quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow); 167 quiz_attempt_save_started($quizobj, $quba, $attempt); 168 $attemptobj = quiz_attempt::create($attempt->id); 169 170 if ($finishattempt) { 171 // Process some responses from the student. 172 $tosubmit = array(1 => array('answer' => '3.14')); 173 $attemptobj->process_submitted_actions(time(), false, $tosubmit); 174 175 // Finish the attempt. 176 $attemptobj->process_finish(time(), false); 177 } 178 return array($quiz, $context, $quizobj, $attempt, $attemptobj, $quba); 179 } else { 180 return array($quiz, $context, $quizobj); 181 } 182 183 } 184 185 /* 186 * Test get quizzes by courses 187 */ 188 public function test_mod_quiz_get_quizzes_by_courses() { 189 global $DB; 190 191 // Create additional course. 192 $course2 = self::getDataGenerator()->create_course(); 193 194 // Second quiz. 195 $record = new \stdClass(); 196 $record->course = $course2->id; 197 $record->intro = '<button>Test with HTML allowed.</button>'; 198 $quiz2 = self::getDataGenerator()->create_module('quiz', $record); 199 200 // Execute real Moodle enrolment as we'll call unenrol() method on the instance later. 201 $enrol = enrol_get_plugin('manual'); 202 $enrolinstances = enrol_get_instances($course2->id, true); 203 foreach ($enrolinstances as $courseenrolinstance) { 204 if ($courseenrolinstance->enrol == "manual") { 205 $instance2 = $courseenrolinstance; 206 break; 207 } 208 } 209 $enrol->enrol_user($instance2, $this->student->id, $this->studentrole->id); 210 211 self::setUser($this->student); 212 213 $returndescription = mod_quiz_external::get_quizzes_by_courses_returns(); 214 215 // Create what we expect to be returned when querying the two courses. 216 // First for the student user. 217 $allusersfields = array('id', 'coursemodule', 'course', 'name', 'intro', 'introformat', 'introfiles', 'timeopen', 218 'timeclose', 'grademethod', 'section', 'visible', 'groupmode', 'groupingid', 219 'attempts', 'timelimit', 'grademethod', 'decimalpoints', 'questiondecimalpoints', 'sumgrades', 220 'grade', 'preferredbehaviour', 'hasfeedback'); 221 $userswithaccessfields = array('attemptonlast', 'reviewattempt', 'reviewcorrectness', 'reviewmarks', 222 'reviewspecificfeedback', 'reviewgeneralfeedback', 'reviewrightanswer', 223 'reviewoverallfeedback', 'questionsperpage', 'navmethod', 224 'browsersecurity', 'delay1', 'delay2', 'showuserpicture', 'showblocks', 225 'completionattemptsexhausted', 'completionpass', 'autosaveperiod', 'hasquestions', 226 'overduehandling', 'graceperiod', 'canredoquestions', 'allowofflineattempts'); 227 $managerfields = array('shuffleanswers', 'timecreated', 'timemodified', 'password', 'subnet'); 228 229 // Add expected coursemodule and other data. 230 $quiz1 = $this->quiz; 231 $quiz1->coursemodule = $quiz1->cmid; 232 $quiz1->introformat = 1; 233 $quiz1->section = 0; 234 $quiz1->visible = true; 235 $quiz1->groupmode = 0; 236 $quiz1->groupingid = 0; 237 $quiz1->hasquestions = 0; 238 $quiz1->hasfeedback = 0; 239 $quiz1->autosaveperiod = get_config('quiz', 'autosaveperiod'); 240 $quiz1->introfiles = []; 241 242 $quiz2->coursemodule = $quiz2->cmid; 243 $quiz2->introformat = 1; 244 $quiz2->section = 0; 245 $quiz2->visible = true; 246 $quiz2->groupmode = 0; 247 $quiz2->groupingid = 0; 248 $quiz2->hasquestions = 0; 249 $quiz2->hasfeedback = 0; 250 $quiz2->autosaveperiod = get_config('quiz', 'autosaveperiod'); 251 $quiz2->introfiles = []; 252 253 foreach (array_merge($allusersfields, $userswithaccessfields) as $field) { 254 $expected1[$field] = $quiz1->{$field}; 255 $expected2[$field] = $quiz2->{$field}; 256 } 257 258 $expectedquizzes = array($expected2, $expected1); 259 260 // Call the external function passing course ids. 261 $result = mod_quiz_external::get_quizzes_by_courses(array($course2->id, $this->course->id)); 262 $result = \external_api::clean_returnvalue($returndescription, $result); 263 264 $this->assertEquals($expectedquizzes, $result['quizzes']); 265 $this->assertCount(0, $result['warnings']); 266 267 // Call the external function without passing course id. 268 $result = mod_quiz_external::get_quizzes_by_courses(); 269 $result = \external_api::clean_returnvalue($returndescription, $result); 270 $this->assertEquals($expectedquizzes, $result['quizzes']); 271 $this->assertCount(0, $result['warnings']); 272 273 // Unenrol user from second course and alter expected quizzes. 274 $enrol->unenrol_user($instance2, $this->student->id); 275 array_shift($expectedquizzes); 276 277 // Call the external function without passing course id. 278 $result = mod_quiz_external::get_quizzes_by_courses(); 279 $result = \external_api::clean_returnvalue($returndescription, $result); 280 $this->assertEquals($expectedquizzes, $result['quizzes']); 281 282 // Call for the second course we unenrolled the user from, expected warning. 283 $result = mod_quiz_external::get_quizzes_by_courses(array($course2->id)); 284 $this->assertCount(1, $result['warnings']); 285 $this->assertEquals('1', $result['warnings'][0]['warningcode']); 286 $this->assertEquals($course2->id, $result['warnings'][0]['itemid']); 287 288 // Now, try as a teacher for getting all the additional fields. 289 self::setUser($this->teacher); 290 291 foreach ($managerfields as $field) { 292 $expectedquizzes[0][$field] = $quiz1->{$field}; 293 } 294 295 $result = mod_quiz_external::get_quizzes_by_courses(); 296 $result = \external_api::clean_returnvalue($returndescription, $result); 297 $this->assertEquals($expectedquizzes, $result['quizzes']); 298 299 // Admin also should get all the information. 300 self::setAdminUser(); 301 302 $result = mod_quiz_external::get_quizzes_by_courses(array($this->course->id)); 303 $result = \external_api::clean_returnvalue($returndescription, $result); 304 $this->assertEquals($expectedquizzes, $result['quizzes']); 305 306 // Now, prevent access. 307 $enrol->enrol_user($instance2, $this->student->id); 308 309 self::setUser($this->student); 310 311 $quiz2->timeclose = time() - DAYSECS; 312 $DB->update_record('quiz', $quiz2); 313 314 $result = mod_quiz_external::get_quizzes_by_courses(); 315 $result = \external_api::clean_returnvalue($returndescription, $result); 316 $this->assertCount(2, $result['quizzes']); 317 // We only see a limited set of fields. 318 $this->assertCount(4, $result['quizzes'][0]); 319 $this->assertEquals($quiz2->id, $result['quizzes'][0]['id']); 320 $this->assertEquals($quiz2->coursemodule, $result['quizzes'][0]['coursemodule']); 321 $this->assertEquals($quiz2->course, $result['quizzes'][0]['course']); 322 $this->assertEquals($quiz2->name, $result['quizzes'][0]['name']); 323 $this->assertEquals($quiz2->course, $result['quizzes'][0]['course']); 324 325 $this->assertFalse(isset($result['quizzes'][0]['timelimit'])); 326 327 } 328 329 /** 330 * Test test_view_quiz 331 */ 332 public function test_view_quiz() { 333 global $DB; 334 335 // Test invalid instance id. 336 try { 337 mod_quiz_external::view_quiz(0); 338 $this->fail('Exception expected due to invalid mod_quiz instance id.'); 339 } catch (\moodle_exception $e) { 340 $this->assertEquals('invalidrecord', $e->errorcode); 341 } 342 343 // Test not-enrolled user. 344 $usernotenrolled = self::getDataGenerator()->create_user(); 345 $this->setUser($usernotenrolled); 346 try { 347 mod_quiz_external::view_quiz($this->quiz->id); 348 $this->fail('Exception expected due to not enrolled user.'); 349 } catch (\moodle_exception $e) { 350 $this->assertEquals('requireloginerror', $e->errorcode); 351 } 352 353 // Test user with full capabilities. 354 $this->setUser($this->student); 355 356 // Trigger and capture the event. 357 $sink = $this->redirectEvents(); 358 359 $result = mod_quiz_external::view_quiz($this->quiz->id); 360 $result = \external_api::clean_returnvalue(mod_quiz_external::view_quiz_returns(), $result); 361 $this->assertTrue($result['status']); 362 363 $events = $sink->get_events(); 364 $this->assertCount(1, $events); 365 $event = array_shift($events); 366 367 // Checking that the event contains the expected values. 368 $this->assertInstanceOf('\mod_quiz\event\course_module_viewed', $event); 369 $this->assertEquals($this->context, $event->get_context()); 370 $moodlequiz = new \moodle_url('/mod/quiz/view.php', array('id' => $this->cm->id)); 371 $this->assertEquals($moodlequiz, $event->get_url()); 372 $this->assertEventContextNotUsed($event); 373 $this->assertNotEmpty($event->get_name()); 374 375 // Test user with no capabilities. 376 // We need a explicit prohibit since this capability is only defined in authenticated user and guest roles. 377 assign_capability('mod/quiz:view', CAP_PROHIBIT, $this->studentrole->id, $this->context->id); 378 // Empty all the caches that may be affected by this change. 379 accesslib_clear_all_caches_for_unit_testing(); 380 \course_modinfo::clear_instance_cache(); 381 382 try { 383 mod_quiz_external::view_quiz($this->quiz->id); 384 $this->fail('Exception expected due to missing capability.'); 385 } catch (\moodle_exception $e) { 386 $this->assertEquals('requireloginerror', $e->errorcode); 387 } 388 389 } 390 391 /** 392 * Test get_user_attempts 393 */ 394 public function test_get_user_attempts() { 395 396 // Create a quiz with one attempt finished. 397 list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true, true); 398 399 $this->setUser($this->student); 400 $result = mod_quiz_external::get_user_attempts($quiz->id); 401 $result = \external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result); 402 403 $this->assertCount(1, $result['attempts']); 404 $this->assertEquals($attempt->id, $result['attempts'][0]['id']); 405 $this->assertEquals($quiz->id, $result['attempts'][0]['quiz']); 406 $this->assertEquals($this->student->id, $result['attempts'][0]['userid']); 407 $this->assertEquals(1, $result['attempts'][0]['attempt']); 408 $this->assertArrayHasKey('sumgrades', $result['attempts'][0]); 409 $this->assertEquals(1.0, $result['attempts'][0]['sumgrades']); 410 411 // Test filters. Only finished. 412 $result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'finished', false); 413 $result = \external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result); 414 415 $this->assertCount(1, $result['attempts']); 416 $this->assertEquals($attempt->id, $result['attempts'][0]['id']); 417 418 // Test filters. All attempts. 419 $result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'all', false); 420 $result = \external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result); 421 422 $this->assertCount(1, $result['attempts']); 423 $this->assertEquals($attempt->id, $result['attempts'][0]['id']); 424 425 // Test filters. Unfinished. 426 $result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'unfinished', false); 427 $result = \external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result); 428 429 $this->assertCount(0, $result['attempts']); 430 431 // Start a new attempt, but not finish it. 432 $timenow = time(); 433 $attempt = quiz_create_attempt($quizobj, 2, false, $timenow, false, $this->student->id); 434 $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); 435 $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); 436 437 quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow); 438 quiz_attempt_save_started($quizobj, $quba, $attempt); 439 440 // Test filters. All attempts. 441 $result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'all', false); 442 $result = \external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result); 443 444 $this->assertCount(2, $result['attempts']); 445 446 // Test filters. Unfinished. 447 $result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'unfinished', false); 448 $result = \external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result); 449 450 $this->assertCount(1, $result['attempts']); 451 452 // Test manager can see user attempts. 453 $this->setUser($this->teacher); 454 $result = mod_quiz_external::get_user_attempts($quiz->id, $this->student->id); 455 $result = \external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result); 456 457 $this->assertCount(1, $result['attempts']); 458 $this->assertEquals($this->student->id, $result['attempts'][0]['userid']); 459 460 $result = mod_quiz_external::get_user_attempts($quiz->id, $this->student->id, 'all'); 461 $result = \external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result); 462 463 $this->assertCount(2, $result['attempts']); 464 $this->assertEquals($this->student->id, $result['attempts'][0]['userid']); 465 466 // Invalid parameters. 467 try { 468 mod_quiz_external::get_user_attempts($quiz->id, $this->student->id, 'INVALID_PARAMETER'); 469 $this->fail('Exception expected due to missing capability.'); 470 } catch (\invalid_parameter_exception $e) { 471 $this->assertEquals('invalidparameter', $e->errorcode); 472 } 473 } 474 475 /** 476 * Test get_user_attempts with marks hidden 477 */ 478 public function test_get_user_attempts_with_marks_hidden() { 479 // Create quiz with one attempt finished and hide the mark. 480 list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions( 481 true, true, 'deferredfeedback', false, 482 ['marksduring' => 0, 'marksimmediately' => 0, 'marksopen' => 0, 'marksclosed' => 0]); 483 484 // Student cannot see the grades. 485 $this->setUser($this->student); 486 $result = mod_quiz_external::get_user_attempts($quiz->id); 487 $result = \external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result); 488 489 $this->assertCount(1, $result['attempts']); 490 $this->assertEquals($attempt->id, $result['attempts'][0]['id']); 491 $this->assertEquals($quiz->id, $result['attempts'][0]['quiz']); 492 $this->assertEquals($this->student->id, $result['attempts'][0]['userid']); 493 $this->assertEquals(1, $result['attempts'][0]['attempt']); 494 $this->assertArrayHasKey('sumgrades', $result['attempts'][0]); 495 $this->assertEquals(null, $result['attempts'][0]['sumgrades']); 496 497 // Test manager can see user grades. 498 $this->setUser($this->teacher); 499 $result = mod_quiz_external::get_user_attempts($quiz->id, $this->student->id); 500 $result = \external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result); 501 502 $this->assertCount(1, $result['attempts']); 503 $this->assertEquals($attempt->id, $result['attempts'][0]['id']); 504 $this->assertEquals($quiz->id, $result['attempts'][0]['quiz']); 505 $this->assertEquals($this->student->id, $result['attempts'][0]['userid']); 506 $this->assertEquals(1, $result['attempts'][0]['attempt']); 507 $this->assertArrayHasKey('sumgrades', $result['attempts'][0]); 508 $this->assertEquals(1.0, $result['attempts'][0]['sumgrades']); 509 } 510 511 /** 512 * Test get_user_best_grade 513 */ 514 public function test_get_user_best_grade() { 515 $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); 516 $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); 517 $questioncat = $questiongenerator->create_question_category(); 518 519 // Create a new quiz. 520 $quizapi1 = $quizgenerator->create_instance([ 521 'name' => 'Test Quiz API 1', 522 'course' => $this->course->id, 523 'sumgrades' => 1 524 ]); 525 $quizapi2 = $quizgenerator->create_instance([ 526 'name' => 'Test Quiz API 2', 527 'course' => $this->course->id, 528 'sumgrades' => 1, 529 'marksduring' => 0, 530 'marksimmediately' => 0, 531 'marksopen' => 0, 532 'marksclosed' => 0 533 ]); 534 535 // Create a question. 536 $question = $questiongenerator->create_question('numerical', null, ['category' => $questioncat->id]); 537 538 // Add question to the quizzes. 539 quiz_add_quiz_question($question->id, $quizapi1); 540 quiz_add_quiz_question($question->id, $quizapi2); 541 542 // Create quiz object. 543 $quizapiobj1 = quiz::create($quizapi1->id, $this->student->id); 544 $quizapiobj2 = quiz::create($quizapi2->id, $this->student->id); 545 546 // Set grade to pass. 547 $item = \grade_item::fetch([ 548 'courseid' => $this->course->id, 549 'itemtype' => 'mod', 550 'itemmodule' => 'quiz', 551 'iteminstance' => $quizapi1->id, 552 'outcomeid' => null 553 ]); 554 $item->gradepass = 80; 555 $item->update(); 556 557 $item = \grade_item::fetch([ 558 'courseid' => $this->course->id, 559 'itemtype' => 'mod', 560 'itemmodule' => 'quiz', 561 'iteminstance' => $quizapi2->id, 562 'outcomeid' => null 563 ]); 564 $item->gradepass = 80; 565 $item->update(); 566 567 // Start the passing attempt. 568 $quba1 = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizapiobj1->get_context()); 569 $quba1->set_preferred_behaviour($quizapiobj1->get_quiz()->preferredbehaviour); 570 571 $quba2 = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizapiobj2->get_context()); 572 $quba2->set_preferred_behaviour($quizapiobj2->get_quiz()->preferredbehaviour); 573 574 // Start the testing for quizapi1 that allow the student to view the grade. 575 576 $this->setUser($this->student); 577 $result = mod_quiz_external::get_user_best_grade($quizapi1->id); 578 $result = \external_api::clean_returnvalue(mod_quiz_external::get_user_best_grade_returns(), $result); 579 580 // No grades yet. 581 $this->assertFalse($result['hasgrade']); 582 $this->assertTrue(!isset($result['grade'])); 583 584 // Start the attempt. 585 $timenow = time(); 586 $attempt = quiz_create_attempt($quizapiobj1, 1, false, $timenow, false, $this->student->id); 587 quiz_start_new_attempt($quizapiobj1, $quba1, $attempt, 1, $timenow); 588 quiz_attempt_save_started($quizapiobj1, $quba1, $attempt); 589 590 // Process some responses from the student. 591 $attemptobj = quiz_attempt::create($attempt->id); 592 $attemptobj->process_submitted_actions($timenow, false, [1 => ['answer' => '3.14']]); 593 594 // Finish the attempt. 595 $attemptobj->process_finish($timenow, false); 596 597 $result = mod_quiz_external::get_user_best_grade($quizapi1->id); 598 $result = \external_api::clean_returnvalue(mod_quiz_external::get_user_best_grade_returns(), $result); 599 600 // Now I have grades. 601 $this->assertTrue($result['hasgrade']); 602 $this->assertEquals(100.0, $result['grade']); 603 $this->assertEquals(80, $result['gradetopass']); 604 605 // We should not see other users grades. 606 $anotherstudent = self::getDataGenerator()->create_user(); 607 $this->getDataGenerator()->enrol_user($anotherstudent->id, $this->course->id, $this->studentrole->id, 'manual'); 608 609 try { 610 mod_quiz_external::get_user_best_grade($quizapi1->id, $anotherstudent->id); 611 $this->fail('Exception expected due to missing capability.'); 612 } catch (\required_capability_exception $e) { 613 $this->assertEquals('nopermissions', $e->errorcode); 614 } 615 616 // Teacher must be able to see student grades. 617 $this->setUser($this->teacher); 618 619 $result = mod_quiz_external::get_user_best_grade($quizapi1->id, $this->student->id); 620 $result = \external_api::clean_returnvalue(mod_quiz_external::get_user_best_grade_returns(), $result); 621 622 $this->assertTrue($result['hasgrade']); 623 $this->assertEquals(100.0, $result['grade']); 624 $this->assertEquals(80, $result['gradetopass']); 625 626 // Invalid user. 627 try { 628 mod_quiz_external::get_user_best_grade($this->quiz->id, -1); 629 $this->fail('Exception expected due to missing capability.'); 630 } catch (\dml_missing_record_exception $e) { 631 $this->assertEquals('invaliduser', $e->errorcode); 632 } 633 634 // End the testing for quizapi1 that allow the student to view the grade. 635 636 // Start the testing for quizapi2 that do not allow the student to view the grade. 637 638 $this->setUser($this->student); 639 $result = mod_quiz_external::get_user_best_grade($quizapi2->id); 640 $result = \external_api::clean_returnvalue(mod_quiz_external::get_user_best_grade_returns(), $result); 641 642 // No grades yet. 643 $this->assertFalse($result['hasgrade']); 644 $this->assertTrue(!isset($result['grade'])); 645 646 // Start the attempt. 647 $timenow = time(); 648 $attempt = quiz_create_attempt($quizapiobj2, 1, false, $timenow, false, $this->student->id); 649 quiz_start_new_attempt($quizapiobj2, $quba2, $attempt, 1, $timenow); 650 quiz_attempt_save_started($quizapiobj2, $quba2, $attempt); 651 652 // Process some responses from the student. 653 $attemptobj = quiz_attempt::create($attempt->id); 654 $attemptobj->process_submitted_actions($timenow, false, [1 => ['answer' => '3.14']]); 655 656 // Finish the attempt. 657 $attemptobj->process_finish($timenow, false); 658 659 $result = mod_quiz_external::get_user_best_grade($quizapi2->id); 660 $result = \external_api::clean_returnvalue(mod_quiz_external::get_user_best_grade_returns(), $result); 661 662 // Now I have grades but I will not be allowed to see it. 663 $this->assertFalse($result['hasgrade']); 664 $this->assertTrue(!isset($result['grade'])); 665 666 // Teacher must be able to see student grades. 667 $this->setUser($this->teacher); 668 669 $result = mod_quiz_external::get_user_best_grade($quizapi2->id, $this->student->id); 670 $result = \external_api::clean_returnvalue(mod_quiz_external::get_user_best_grade_returns(), $result); 671 672 $this->assertTrue($result['hasgrade']); 673 $this->assertEquals(100.0, $result['grade']); 674 675 // End the testing for quizapi2 that do not allow the student to view the grade. 676 677 } 678 /** 679 * Test get_combined_review_options. 680 * This is a basic test, this is already tested in mod_quiz_display_options_testcase. 681 */ 682 public function test_get_combined_review_options() { 683 global $DB; 684 685 // Create a new quiz with attempts. 686 $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); 687 $data = array('course' => $this->course->id, 688 'sumgrades' => 1); 689 $quiz = $quizgenerator->create_instance($data); 690 691 // Create a couple of questions. 692 $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); 693 694 $cat = $questiongenerator->create_question_category(); 695 $question = $questiongenerator->create_question('numerical', null, array('category' => $cat->id)); 696 quiz_add_quiz_question($question->id, $quiz); 697 698 $quizobj = quiz::create($quiz->id, $this->student->id); 699 700 // Set grade to pass. 701 $item = \grade_item::fetch(array('courseid' => $this->course->id, 'itemtype' => 'mod', 702 'itemmodule' => 'quiz', 'iteminstance' => $quiz->id, 'outcomeid' => null)); 703 $item->gradepass = 80; 704 $item->update(); 705 706 // Start the passing attempt. 707 $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); 708 $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); 709 710 $timenow = time(); 711 $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $this->student->id); 712 quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow); 713 quiz_attempt_save_started($quizobj, $quba, $attempt); 714 715 $this->setUser($this->student); 716 717 $result = mod_quiz_external::get_combined_review_options($quiz->id); 718 $result = \external_api::clean_returnvalue(mod_quiz_external::get_combined_review_options_returns(), $result); 719 720 // Expected values. 721 $expected = array( 722 "someoptions" => array( 723 array("name" => "feedback", "value" => 1), 724 array("name" => "generalfeedback", "value" => 1), 725 array("name" => "rightanswer", "value" => 1), 726 array("name" => "overallfeedback", "value" => 0), 727 array("name" => "marks", "value" => 2), 728 ), 729 "alloptions" => array( 730 array("name" => "feedback", "value" => 1), 731 array("name" => "generalfeedback", "value" => 1), 732 array("name" => "rightanswer", "value" => 1), 733 array("name" => "overallfeedback", "value" => 0), 734 array("name" => "marks", "value" => 2), 735 ), 736 "warnings" => [], 737 ); 738 739 $this->assertEquals($expected, $result); 740 741 // Now, finish the attempt. 742 $attemptobj = quiz_attempt::create($attempt->id); 743 $attemptobj->process_finish($timenow, false); 744 745 $expected = array( 746 "someoptions" => array( 747 array("name" => "feedback", "value" => 1), 748 array("name" => "generalfeedback", "value" => 1), 749 array("name" => "rightanswer", "value" => 1), 750 array("name" => "overallfeedback", "value" => 1), 751 array("name" => "marks", "value" => 2), 752 ), 753 "alloptions" => array( 754 array("name" => "feedback", "value" => 1), 755 array("name" => "generalfeedback", "value" => 1), 756 array("name" => "rightanswer", "value" => 1), 757 array("name" => "overallfeedback", "value" => 1), 758 array("name" => "marks", "value" => 2), 759 ), 760 "warnings" => [], 761 ); 762 763 // We should see now the overall feedback. 764 $result = mod_quiz_external::get_combined_review_options($quiz->id); 765 $result = \external_api::clean_returnvalue(mod_quiz_external::get_combined_review_options_returns(), $result); 766 $this->assertEquals($expected, $result); 767 768 // Start a new attempt, but not finish it. 769 $timenow = time(); 770 $attempt = quiz_create_attempt($quizobj, 2, false, $timenow, false, $this->student->id); 771 $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); 772 $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); 773 quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow); 774 quiz_attempt_save_started($quizobj, $quba, $attempt); 775 776 $expected = array( 777 "someoptions" => array( 778 array("name" => "feedback", "value" => 1), 779 array("name" => "generalfeedback", "value" => 1), 780 array("name" => "rightanswer", "value" => 1), 781 array("name" => "overallfeedback", "value" => 1), 782 array("name" => "marks", "value" => 2), 783 ), 784 "alloptions" => array( 785 array("name" => "feedback", "value" => 1), 786 array("name" => "generalfeedback", "value" => 1), 787 array("name" => "rightanswer", "value" => 1), 788 array("name" => "overallfeedback", "value" => 0), 789 array("name" => "marks", "value" => 2), 790 ), 791 "warnings" => [], 792 ); 793 794 $result = mod_quiz_external::get_combined_review_options($quiz->id); 795 $result = \external_api::clean_returnvalue(mod_quiz_external::get_combined_review_options_returns(), $result); 796 $this->assertEquals($expected, $result); 797 798 // Teacher, for see student options. 799 $this->setUser($this->teacher); 800 801 $result = mod_quiz_external::get_combined_review_options($quiz->id, $this->student->id); 802 $result = \external_api::clean_returnvalue(mod_quiz_external::get_combined_review_options_returns(), $result); 803 804 $this->assertEquals($expected, $result); 805 806 // Invalid user. 807 try { 808 mod_quiz_external::get_combined_review_options($quiz->id, -1); 809 $this->fail('Exception expected due to missing capability.'); 810 } catch (\dml_missing_record_exception $e) { 811 $this->assertEquals('invaliduser', $e->errorcode); 812 } 813 } 814 815 /** 816 * Test start_attempt 817 */ 818 public function test_start_attempt() { 819 global $DB; 820 821 // Create a new quiz with questions. 822 list($quiz, $context, $quizobj) = $this->create_quiz_with_questions(); 823 824 $this->setUser($this->student); 825 826 // Try to open attempt in closed quiz. 827 $quiz->timeopen = time() - WEEKSECS; 828 $quiz->timeclose = time() - DAYSECS; 829 $DB->update_record('quiz', $quiz); 830 $result = mod_quiz_external::start_attempt($quiz->id); 831 $result = \external_api::clean_returnvalue(mod_quiz_external::start_attempt_returns(), $result); 832 833 $this->assertEquals([], $result['attempt']); 834 $this->assertCount(1, $result['warnings']); 835 836 // Now with a password. 837 $quiz->timeopen = 0; 838 $quiz->timeclose = 0; 839 $quiz->password = 'abc'; 840 $DB->update_record('quiz', $quiz); 841 842 try { 843 mod_quiz_external::start_attempt($quiz->id, array(array("name" => "quizpassword", "value" => 'bad'))); 844 $this->fail('Exception expected due to invalid passwod.'); 845 } catch (\moodle_exception $e) { 846 $this->assertEquals(get_string('passworderror', 'quizaccess_password'), $e->errorcode); 847 } 848 849 // Now, try everything correct. 850 $result = mod_quiz_external::start_attempt($quiz->id, array(array("name" => "quizpassword", "value" => 'abc'))); 851 $result = \external_api::clean_returnvalue(mod_quiz_external::start_attempt_returns(), $result); 852 853 $this->assertEquals(1, $result['attempt']['attempt']); 854 $this->assertEquals($this->student->id, $result['attempt']['userid']); 855 $this->assertEquals($quiz->id, $result['attempt']['quiz']); 856 $this->assertCount(0, $result['warnings']); 857 $attemptid = $result['attempt']['id']; 858 859 // We are good, try to start a new attempt now. 860 861 try { 862 mod_quiz_external::start_attempt($quiz->id, array(array("name" => "quizpassword", "value" => 'abc'))); 863 $this->fail('Exception expected due to attempt not finished.'); 864 } catch (\moodle_quiz_exception $e) { 865 $this->assertEquals('attemptstillinprogress', $e->errorcode); 866 } 867 868 // Finish the started attempt. 869 870 // Process some responses from the student. 871 $timenow = time(); 872 $attemptobj = quiz_attempt::create($attemptid); 873 $tosubmit = array(1 => array('answer' => '3.14')); 874 $attemptobj->process_submitted_actions($timenow, false, $tosubmit); 875 876 // Finish the attempt. 877 $attemptobj = quiz_attempt::create($attemptid); 878 $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question()); 879 $attemptobj->process_finish($timenow, false); 880 881 // We should be able to start a new attempt. 882 $result = mod_quiz_external::start_attempt($quiz->id, array(array("name" => "quizpassword", "value" => 'abc'))); 883 $result = \external_api::clean_returnvalue(mod_quiz_external::start_attempt_returns(), $result); 884 885 $this->assertEquals(2, $result['attempt']['attempt']); 886 $this->assertEquals($this->student->id, $result['attempt']['userid']); 887 $this->assertEquals($quiz->id, $result['attempt']['quiz']); 888 $this->assertCount(0, $result['warnings']); 889 890 // Test user with no capabilities. 891 // We need a explicit prohibit since this capability is only defined in authenticated user and guest roles. 892 assign_capability('mod/quiz:attempt', CAP_PROHIBIT, $this->studentrole->id, $context->id); 893 // Empty all the caches that may be affected by this change. 894 accesslib_clear_all_caches_for_unit_testing(); 895 \course_modinfo::clear_instance_cache(); 896 897 try { 898 mod_quiz_external::start_attempt($quiz->id); 899 $this->fail('Exception expected due to missing capability.'); 900 } catch (\required_capability_exception $e) { 901 $this->assertEquals('nopermissions', $e->errorcode); 902 } 903 904 } 905 906 /** 907 * Test validate_attempt 908 */ 909 public function test_validate_attempt() { 910 global $DB; 911 912 // Create a new quiz with one attempt started. 913 list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true); 914 915 $this->setUser($this->student); 916 917 // Invalid attempt. 918 try { 919 $params = array('attemptid' => -1, 'page' => 0); 920 testable_mod_quiz_external::validate_attempt($params); 921 $this->fail('Exception expected due to invalid attempt id.'); 922 } catch (\dml_missing_record_exception $e) { 923 $this->assertEquals('invalidrecord', $e->errorcode); 924 } 925 926 // Test OK case. 927 $params = array('attemptid' => $attempt->id, 'page' => 0); 928 $result = testable_mod_quiz_external::validate_attempt($params); 929 $this->assertEquals($attempt->id, $result[0]->get_attempt()->id); 930 $this->assertEquals([], $result[1]); 931 932 // Test with preflight data. 933 $quiz->password = 'abc'; 934 $DB->update_record('quiz', $quiz); 935 936 try { 937 $params = array('attemptid' => $attempt->id, 'page' => 0, 938 'preflightdata' => array(array("name" => "quizpassword", "value" => 'bad'))); 939 testable_mod_quiz_external::validate_attempt($params); 940 $this->fail('Exception expected due to invalid passwod.'); 941 } catch (\moodle_exception $e) { 942 $this->assertEquals(get_string('passworderror', 'quizaccess_password'), $e->errorcode); 943 } 944 945 // Now, try everything correct. 946 $params['preflightdata'][0]['value'] = 'abc'; 947 $result = testable_mod_quiz_external::validate_attempt($params); 948 $this->assertEquals($attempt->id, $result[0]->get_attempt()->id); 949 $this->assertEquals([], $result[1]); 950 951 // Page out of range. 952 $DB->update_record('quiz', $quiz); 953 $params['page'] = 4; 954 try { 955 testable_mod_quiz_external::validate_attempt($params); 956 $this->fail('Exception expected due to page out of range.'); 957 } catch (\moodle_quiz_exception $e) { 958 $this->assertEquals('Invalid page number', $e->errorcode); 959 } 960 961 $params['page'] = 0; 962 // Try to open attempt in closed quiz. 963 $quiz->timeopen = time() - WEEKSECS; 964 $quiz->timeclose = time() - DAYSECS; 965 $DB->update_record('quiz', $quiz); 966 967 // This should work, ommit access rules. 968 testable_mod_quiz_external::validate_attempt($params, false); 969 970 // Get a generic error because prior to checking the dates the attempt is closed. 971 try { 972 testable_mod_quiz_external::validate_attempt($params); 973 $this->fail('Exception expected due to passed dates.'); 974 } catch (\moodle_quiz_exception $e) { 975 $this->assertEquals('attempterror', $e->errorcode); 976 } 977 978 // Finish the attempt. 979 $attemptobj = quiz_attempt::create($attempt->id); 980 $attemptobj->process_finish(time(), false); 981 982 try { 983 testable_mod_quiz_external::validate_attempt($params, false); 984 $this->fail('Exception expected due to attempt finished.'); 985 } catch (\moodle_quiz_exception $e) { 986 $this->assertEquals('attemptalreadyclosed', $e->errorcode); 987 } 988 989 // Test user with no capabilities. 990 // We need a explicit prohibit since this capability is only defined in authenticated user and guest roles. 991 assign_capability('mod/quiz:attempt', CAP_PROHIBIT, $this->studentrole->id, $context->id); 992 // Empty all the caches that may be affected by this change. 993 accesslib_clear_all_caches_for_unit_testing(); 994 \course_modinfo::clear_instance_cache(); 995 996 try { 997 testable_mod_quiz_external::validate_attempt($params); 998 $this->fail('Exception expected due to missing permissions.'); 999 } catch (\required_capability_exception $e) { 1000 $this->assertEquals('nopermissions', $e->errorcode); 1001 } 1002 1003 // Now try with a different user. 1004 $this->setUser($this->teacher); 1005 1006 $params['page'] = 0; 1007 try { 1008 testable_mod_quiz_external::validate_attempt($params); 1009 $this->fail('Exception expected due to not your attempt.'); 1010 } catch (\moodle_quiz_exception $e) { 1011 $this->assertEquals('notyourattempt', $e->errorcode); 1012 } 1013 } 1014 1015 /** 1016 * Test get_attempt_data 1017 */ 1018 public function test_get_attempt_data() { 1019 global $DB; 1020 1021 $timenow = time(); 1022 // Create a new quiz with one attempt started. 1023 list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true); 1024 1025 // Set correctness mask so questions state can be fetched only after finishing the attempt. 1026 $DB->set_field('quiz', 'reviewcorrectness', mod_quiz_display_options::IMMEDIATELY_AFTER, array('id' => $quiz->id)); 1027 1028 $quizobj = $attemptobj->get_quizobj(); 1029 $quizobj->preload_questions(); 1030 $quizobj->load_questions(); 1031 $questions = $quizobj->get_questions(); 1032 1033 $this->setUser($this->student); 1034 1035 // We receive one question per page. 1036 $result = mod_quiz_external::get_attempt_data($attempt->id, 0); 1037 $result = \external_api::clean_returnvalue(mod_quiz_external::get_attempt_data_returns(), $result); 1038 1039 $this->assertEquals($attempt, (object) $result['attempt']); 1040 $this->assertEquals(1, $result['nextpage']); 1041 $this->assertCount(0, $result['messages']); 1042 $this->assertCount(1, $result['questions']); 1043 $this->assertEquals(1, $result['questions'][0]['slot']); 1044 $this->assertEquals(1, $result['questions'][0]['number']); 1045 $this->assertEquals('numerical', $result['questions'][0]['type']); 1046 $this->assertArrayNotHasKey('state', $result['questions'][0]); // We don't receive the state yet. 1047 $this->assertEquals(get_string('notyetanswered', 'question'), $result['questions'][0]['status']); 1048 $this->assertFalse($result['questions'][0]['flagged']); 1049 $this->assertEquals(0, $result['questions'][0]['page']); 1050 $this->assertEmpty($result['questions'][0]['mark']); 1051 $this->assertEquals(1, $result['questions'][0]['maxmark']); 1052 $this->assertEquals(1, $result['questions'][0]['sequencecheck']); 1053 $this->assertGreaterThanOrEqual($timenow, $result['questions'][0]['lastactiontime']); 1054 $this->assertEquals(false, $result['questions'][0]['hasautosavedstep']); 1055 1056 // Now try the last page. 1057 $result = mod_quiz_external::get_attempt_data($attempt->id, 1); 1058 $result = \external_api::clean_returnvalue(mod_quiz_external::get_attempt_data_returns(), $result); 1059 1060 $this->assertEquals($attempt, (object) $result['attempt']); 1061 $this->assertEquals(-1, $result['nextpage']); 1062 $this->assertCount(0, $result['messages']); 1063 $this->assertCount(1, $result['questions']); 1064 $this->assertEquals(2, $result['questions'][0]['slot']); 1065 $this->assertEquals(2, $result['questions'][0]['number']); 1066 $this->assertEquals('numerical', $result['questions'][0]['type']); 1067 $this->assertArrayNotHasKey('state', $result['questions'][0]); // We don't receive the state yet. 1068 $this->assertEquals(get_string('notyetanswered', 'question'), $result['questions'][0]['status']); 1069 $this->assertFalse($result['questions'][0]['flagged']); 1070 $this->assertEquals(1, $result['questions'][0]['page']); 1071 $this->assertEquals(1, $result['questions'][0]['sequencecheck']); 1072 $this->assertGreaterThanOrEqual($timenow, $result['questions'][0]['lastactiontime']); 1073 $this->assertEquals(false, $result['questions'][0]['hasautosavedstep']); 1074 1075 // Finish previous attempt. 1076 $attemptobj->process_finish(time(), false); 1077 1078 // Now we should receive the question state. 1079 $result = mod_quiz_external::get_attempt_review($attempt->id, 1); 1080 $result = \external_api::clean_returnvalue(mod_quiz_external::get_attempt_review_returns(), $result); 1081 $this->assertEquals('gaveup', $result['questions'][0]['state']); 1082 1083 // Change setting and expect two pages. 1084 $quiz->questionsperpage = 4; 1085 $DB->update_record('quiz', $quiz); 1086 quiz_repaginate_questions($quiz->id, $quiz->questionsperpage); 1087 1088 // Start with new attempt with the new layout. 1089 $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); 1090 $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); 1091 1092 $timenow = time(); 1093 $attempt = quiz_create_attempt($quizobj, 2, false, $timenow, false, $this->student->id); 1094 quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow); 1095 quiz_attempt_save_started($quizobj, $quba, $attempt); 1096 1097 // We receive two questions per page. 1098 $result = mod_quiz_external::get_attempt_data($attempt->id, 0); 1099 $result = \external_api::clean_returnvalue(mod_quiz_external::get_attempt_data_returns(), $result); 1100 $this->assertCount(2, $result['questions']); 1101 $this->assertEquals(-1, $result['nextpage']); 1102 1103 // Check questions looks good. 1104 $found = 0; 1105 foreach ($questions as $question) { 1106 foreach ($result['questions'] as $rquestion) { 1107 if ($rquestion['slot'] == $question->slot) { 1108 $this->assertTrue(strpos($rquestion['html'], "qid=$question->id") !== false); 1109 $found++; 1110 } 1111 } 1112 } 1113 $this->assertEquals(2, $found); 1114 1115 } 1116 1117 /** 1118 * Test get_attempt_data with blocked questions. 1119 * @since 3.2 1120 */ 1121 public function test_get_attempt_data_with_blocked_questions() { 1122 global $DB; 1123 1124 // Create a new quiz with one attempt started and using immediatefeedback. 1125 list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions( 1126 true, false, 'immediatefeedback'); 1127 1128 $quizobj = $attemptobj->get_quizobj(); 1129 1130 // Make second question blocked by the first one. 1131 $structure = $quizobj->get_structure(); 1132 $slots = $structure->get_slots(); 1133 $structure->update_question_dependency(end($slots)->id, true); 1134 1135 $quizobj->preload_questions(); 1136 $quizobj->load_questions(); 1137 $questions = $quizobj->get_questions(); 1138 1139 $this->setUser($this->student); 1140 1141 // We receive one question per page. 1142 $result = mod_quiz_external::get_attempt_data($attempt->id, 0); 1143 $result = \external_api::clean_returnvalue(mod_quiz_external::get_attempt_data_returns(), $result); 1144 1145 $this->assertEquals($attempt, (object) $result['attempt']); 1146 $this->assertCount(1, $result['questions']); 1147 $this->assertEquals(1, $result['questions'][0]['slot']); 1148 $this->assertEquals(1, $result['questions'][0]['number']); 1149 $this->assertEquals(false, $result['questions'][0]['blockedbyprevious']); 1150 1151 // Now try the last page. 1152 $result = mod_quiz_external::get_attempt_data($attempt->id, 1); 1153 $result = \external_api::clean_returnvalue(mod_quiz_external::get_attempt_data_returns(), $result); 1154 1155 $this->assertEquals($attempt, (object) $result['attempt']); 1156 $this->assertCount(1, $result['questions']); 1157 $this->assertEquals(2, $result['questions'][0]['slot']); 1158 $this->assertEquals(2, $result['questions'][0]['number']); 1159 $this->assertEquals(true, $result['questions'][0]['blockedbyprevious']); 1160 } 1161 1162 /** 1163 * Test get_attempt_summary 1164 */ 1165 public function test_get_attempt_summary() { 1166 1167 $timenow = time(); 1168 // Create a new quiz with one attempt started. 1169 list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true); 1170 1171 $this->setUser($this->student); 1172 $result = mod_quiz_external::get_attempt_summary($attempt->id); 1173 $result = \external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result); 1174 1175 // Check the state, flagged and mark data is correct. 1176 $this->assertEquals('todo', $result['questions'][0]['state']); 1177 $this->assertEquals('todo', $result['questions'][1]['state']); 1178 $this->assertEquals(1, $result['questions'][0]['number']); 1179 $this->assertEquals(2, $result['questions'][1]['number']); 1180 $this->assertFalse($result['questions'][0]['flagged']); 1181 $this->assertFalse($result['questions'][1]['flagged']); 1182 $this->assertEmpty($result['questions'][0]['mark']); 1183 $this->assertEmpty($result['questions'][1]['mark']); 1184 $this->assertEquals(1, $result['questions'][0]['sequencecheck']); 1185 $this->assertEquals(1, $result['questions'][1]['sequencecheck']); 1186 $this->assertGreaterThanOrEqual($timenow, $result['questions'][0]['lastactiontime']); 1187 $this->assertGreaterThanOrEqual($timenow, $result['questions'][1]['lastactiontime']); 1188 $this->assertEquals(false, $result['questions'][0]['hasautosavedstep']); 1189 $this->assertEquals(false, $result['questions'][1]['hasautosavedstep']); 1190 1191 // Check question options. 1192 $this->assertNotEmpty(5, $result['questions'][0]['settings']); 1193 // Check at least some settings returned. 1194 $this->assertCount(4, (array) json_decode($result['questions'][0]['settings'])); 1195 1196 // Submit a response for the first question. 1197 $tosubmit = array(1 => array('answer' => '3.14')); 1198 $attemptobj->process_submitted_actions(time(), false, $tosubmit); 1199 $result = mod_quiz_external::get_attempt_summary($attempt->id); 1200 $result = \external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result); 1201 1202 // Check it's marked as completed only the first one. 1203 $this->assertEquals('complete', $result['questions'][0]['state']); 1204 $this->assertEquals('todo', $result['questions'][1]['state']); 1205 $this->assertEquals(1, $result['questions'][0]['number']); 1206 $this->assertEquals(2, $result['questions'][1]['number']); 1207 $this->assertFalse($result['questions'][0]['flagged']); 1208 $this->assertFalse($result['questions'][1]['flagged']); 1209 $this->assertEmpty($result['questions'][0]['mark']); 1210 $this->assertEmpty($result['questions'][1]['mark']); 1211 $this->assertEquals(2, $result['questions'][0]['sequencecheck']); 1212 $this->assertEquals(1, $result['questions'][1]['sequencecheck']); 1213 $this->assertGreaterThanOrEqual($timenow, $result['questions'][0]['lastactiontime']); 1214 $this->assertGreaterThanOrEqual($timenow, $result['questions'][1]['lastactiontime']); 1215 $this->assertEquals(false, $result['questions'][0]['hasautosavedstep']); 1216 $this->assertEquals(false, $result['questions'][1]['hasautosavedstep']); 1217 1218 } 1219 1220 /** 1221 * Test save_attempt 1222 */ 1223 public function test_save_attempt() { 1224 1225 $timenow = time(); 1226 // Create a new quiz with one attempt started. 1227 list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true); 1228 1229 // Response for slot 1. 1230 $prefix = $quba->get_field_prefix(1); 1231 $data = array( 1232 array('name' => 'slots', 'value' => 1), 1233 array('name' => $prefix . ':sequencecheck', 1234 'value' => $attemptobj->get_question_attempt(1)->get_sequence_check_count()), 1235 array('name' => $prefix . 'answer', 'value' => 1), 1236 ); 1237 1238 $this->setUser($this->student); 1239 1240 $result = mod_quiz_external::save_attempt($attempt->id, $data); 1241 $result = \external_api::clean_returnvalue(mod_quiz_external::save_attempt_returns(), $result); 1242 $this->assertTrue($result['status']); 1243 1244 // Now, get the summary. 1245 $result = mod_quiz_external::get_attempt_summary($attempt->id); 1246 $result = \external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result); 1247 1248 // Check it's marked as completed only the first one. 1249 $this->assertEquals('complete', $result['questions'][0]['state']); 1250 $this->assertEquals('todo', $result['questions'][1]['state']); 1251 $this->assertEquals(1, $result['questions'][0]['number']); 1252 $this->assertEquals(2, $result['questions'][1]['number']); 1253 $this->assertFalse($result['questions'][0]['flagged']); 1254 $this->assertFalse($result['questions'][1]['flagged']); 1255 $this->assertEmpty($result['questions'][0]['mark']); 1256 $this->assertEmpty($result['questions'][1]['mark']); 1257 $this->assertEquals(1, $result['questions'][0]['sequencecheck']); 1258 $this->assertEquals(1, $result['questions'][1]['sequencecheck']); 1259 $this->assertGreaterThanOrEqual($timenow, $result['questions'][0]['lastactiontime']); 1260 $this->assertGreaterThanOrEqual($timenow, $result['questions'][1]['lastactiontime']); 1261 $this->assertEquals(true, $result['questions'][0]['hasautosavedstep']); 1262 $this->assertEquals(false, $result['questions'][1]['hasautosavedstep']); 1263 1264 // Now, second slot. 1265 $prefix = $quba->get_field_prefix(2); 1266 $data = array( 1267 array('name' => 'slots', 'value' => 2), 1268 array('name' => $prefix . ':sequencecheck', 1269 'value' => $attemptobj->get_question_attempt(1)->get_sequence_check_count()), 1270 array('name' => $prefix . 'answer', 'value' => 1), 1271 ); 1272 1273 $result = mod_quiz_external::save_attempt($attempt->id, $data); 1274 $result = \external_api::clean_returnvalue(mod_quiz_external::save_attempt_returns(), $result); 1275 $this->assertTrue($result['status']); 1276 1277 // Now, get the summary. 1278 $result = mod_quiz_external::get_attempt_summary($attempt->id); 1279 $result = \external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result); 1280 1281 // Check it's marked as completed only the first one. 1282 $this->assertEquals('complete', $result['questions'][0]['state']); 1283 $this->assertEquals(1, $result['questions'][0]['sequencecheck']); 1284 $this->assertEquals('complete', $result['questions'][1]['state']); 1285 $this->assertEquals(1, $result['questions'][1]['sequencecheck']); 1286 1287 } 1288 1289 /** 1290 * Test process_attempt 1291 */ 1292 public function test_process_attempt() { 1293 global $DB; 1294 1295 $timenow = time(); 1296 // Create a new quiz with three questions and one attempt started. 1297 list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true, false, 1298 'deferredfeedback', true); 1299 1300 // Response for slot 1. 1301 $prefix = $quba->get_field_prefix(1); 1302 $data = array( 1303 array('name' => 'slots', 'value' => 1), 1304 array('name' => $prefix . ':sequencecheck', 1305 'value' => $attemptobj->get_question_attempt(1)->get_sequence_check_count()), 1306 array('name' => $prefix . 'answer', 'value' => 1), 1307 ); 1308 1309 $this->setUser($this->student); 1310 1311 $result = mod_quiz_external::process_attempt($attempt->id, $data); 1312 $result = \external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result); 1313 $this->assertEquals(quiz_attempt::IN_PROGRESS, $result['state']); 1314 1315 $result = mod_quiz_external::get_attempt_data($attempt->id, 2); 1316 1317 // Now, get the summary. 1318 $result = mod_quiz_external::get_attempt_summary($attempt->id); 1319 $result = \external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result); 1320 $this->assertDebuggingCalled(); // Expect $PAGE->set_url debugging. 1321 1322 // Check it's marked as completed only the first one. 1323 $this->assertEquals('complete', $result['questions'][0]['state']); 1324 $this->assertEquals('todo', $result['questions'][1]['state']); 1325 $this->assertEquals(1, $result['questions'][0]['number']); 1326 $this->assertEquals(2, $result['questions'][1]['number']); 1327 $this->assertFalse($result['questions'][0]['flagged']); 1328 $this->assertFalse($result['questions'][1]['flagged']); 1329 $this->assertEmpty($result['questions'][0]['mark']); 1330 $this->assertEmpty($result['questions'][1]['mark']); 1331 $this->assertEquals(2, $result['questions'][0]['sequencecheck']); 1332 $this->assertEquals(2, $result['questions'][0]['sequencecheck']); 1333 $this->assertGreaterThanOrEqual($timenow, $result['questions'][0]['lastactiontime']); 1334 $this->assertGreaterThanOrEqual($timenow, $result['questions'][0]['lastactiontime']); 1335 $this->assertEquals(false, $result['questions'][0]['hasautosavedstep']); 1336 $this->assertEquals(false, $result['questions'][0]['hasautosavedstep']); 1337 1338 // Now, second slot. 1339 $prefix = $quba->get_field_prefix(2); 1340 $data = array( 1341 array('name' => 'slots', 'value' => 2), 1342 array('name' => $prefix . ':sequencecheck', 1343 'value' => $attemptobj->get_question_attempt(1)->get_sequence_check_count()), 1344 array('name' => $prefix . 'answer', 'value' => 1), 1345 array('name' => $prefix . ':flagged', 'value' => 1), 1346 ); 1347 1348 $result = mod_quiz_external::process_attempt($attempt->id, $data); 1349 $result = \external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result); 1350 $this->assertEquals(quiz_attempt::IN_PROGRESS, $result['state']); 1351 1352 // Now, get the summary. 1353 $result = mod_quiz_external::get_attempt_summary($attempt->id); 1354 $result = \external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result); 1355 1356 // Check it's marked as completed the two first questions. 1357 $this->assertEquals('complete', $result['questions'][0]['state']); 1358 $this->assertEquals('complete', $result['questions'][1]['state']); 1359 $this->assertFalse($result['questions'][0]['flagged']); 1360 $this->assertTrue($result['questions'][1]['flagged']); 1361 1362 // Add files in the attachment response. 1363 $draftitemid = file_get_unused_draft_itemid(); 1364 $filerecordinline = array( 1365 'contextid' => \context_user::instance($this->student->id)->id, 1366 'component' => 'user', 1367 'filearea' => 'draft', 1368 'itemid' => $draftitemid, 1369 'filepath' => '/', 1370 'filename' => 'faketxt.txt', 1371 ); 1372 $fs = get_file_storage(); 1373 $fs->create_file_from_string($filerecordinline, 'fake txt contents 1.'); 1374 1375 // Last slot. 1376 $prefix = $quba->get_field_prefix(3); 1377 $data = array( 1378 array('name' => 'slots', 'value' => 3), 1379 array('name' => $prefix . ':sequencecheck', 1380 'value' => $attemptobj->get_question_attempt(1)->get_sequence_check_count()), 1381 array('name' => $prefix . 'answer', 'value' => 'Some test'), 1382 array('name' => $prefix . 'answerformat', 'value' => FORMAT_HTML), 1383 array('name' => $prefix . 'attachments', 'value' => $draftitemid), 1384 ); 1385 1386 $result = mod_quiz_external::process_attempt($attempt->id, $data); 1387 $result = \external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result); 1388 $this->assertEquals(quiz_attempt::IN_PROGRESS, $result['state']); 1389 1390 // Now, get the summary. 1391 $result = mod_quiz_external::get_attempt_summary($attempt->id); 1392 $result = \external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result); 1393 1394 $this->assertEquals('complete', $result['questions'][0]['state']); 1395 $this->assertEquals('complete', $result['questions'][1]['state']); 1396 $this->assertEquals('complete', $result['questions'][2]['state']); 1397 $this->assertFalse($result['questions'][0]['flagged']); 1398 $this->assertTrue($result['questions'][1]['flagged']); 1399 $this->assertFalse($result['questions'][2]['flagged']); 1400 1401 // Check submitted files are there. 1402 $this->assertCount(1, $result['questions'][2]['responsefileareas']); 1403 $this->assertEquals('attachments', $result['questions'][2]['responsefileareas'][0]['area']); 1404 $this->assertCount(1, $result['questions'][2]['responsefileareas'][0]['files']); 1405 $this->assertEquals($filerecordinline['filename'], $result['questions'][2]['responsefileareas'][0]['files'][0]['filename']); 1406 1407 // Finish the attempt. 1408 $sink = $this->redirectMessages(); 1409 $result = mod_quiz_external::process_attempt($attempt->id, array(), true); 1410 $result = \external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result); 1411 $this->assertEquals(quiz_attempt::FINISHED, $result['state']); 1412 $messages = $sink->get_messages(); 1413 $message = reset($messages); 1414 $sink->close(); 1415 // Test customdata. 1416 if (!empty($message->customdata)) { 1417 $customdata = json_decode($message->customdata); 1418 $this->assertEquals($quizobj->get_quizid(), $customdata->instance); 1419 $this->assertEquals($quizobj->get_cmid(), $customdata->cmid); 1420 $this->assertEquals($attempt->id, $customdata->attemptid); 1421 $this->assertObjectHasAttribute('notificationiconurl', $customdata); 1422 } 1423 1424 // Start new attempt. 1425 $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); 1426 $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); 1427 1428 $timenow = time(); 1429 $attempt = quiz_create_attempt($quizobj, 2, false, $timenow, false, $this->student->id); 1430 quiz_start_new_attempt($quizobj, $quba, $attempt, 2, $timenow); 1431 quiz_attempt_save_started($quizobj, $quba, $attempt); 1432 1433 // Force grace period, attempt going to overdue. 1434 $quiz->timeclose = $timenow - 10; 1435 $quiz->graceperiod = 60; 1436 $quiz->overduehandling = 'graceperiod'; 1437 $DB->update_record('quiz', $quiz); 1438 1439 $result = mod_quiz_external::process_attempt($attempt->id, array()); 1440 $result = \external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result); 1441 $this->assertEquals(quiz_attempt::OVERDUE, $result['state']); 1442 1443 // Force grace period for time limit. 1444 $quiz->timeclose = 0; 1445 $quiz->timelimit = 1; 1446 $quiz->graceperiod = 60; 1447 $quiz->overduehandling = 'graceperiod'; 1448 $DB->update_record('quiz', $quiz); 1449 1450 $timenow = time(); 1451 $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); 1452 $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); 1453 $attempt = quiz_create_attempt($quizobj, 3, 2, $timenow - 10, false, $this->student->id); 1454 quiz_start_new_attempt($quizobj, $quba, $attempt, 2, $timenow - 10); 1455 quiz_attempt_save_started($quizobj, $quba, $attempt); 1456 1457 $result = mod_quiz_external::process_attempt($attempt->id, array()); 1458 $result = \external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result); 1459 $this->assertEquals(quiz_attempt::OVERDUE, $result['state']); 1460 1461 // New attempt. 1462 $timenow = time(); 1463 $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); 1464 $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); 1465 $attempt = quiz_create_attempt($quizobj, 4, 3, $timenow, false, $this->student->id); 1466 quiz_start_new_attempt($quizobj, $quba, $attempt, 3, $timenow); 1467 quiz_attempt_save_started($quizobj, $quba, $attempt); 1468 1469 // Force abandon. 1470 $quiz->timeclose = $timenow - HOURSECS; 1471 $DB->update_record('quiz', $quiz); 1472 1473 $result = mod_quiz_external::process_attempt($attempt->id, array()); 1474 $result = \external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result); 1475 $this->assertEquals(quiz_attempt::ABANDONED, $result['state']); 1476 1477 } 1478 1479 /** 1480 * Test validate_attempt_review 1481 */ 1482 public function test_validate_attempt_review() { 1483 global $DB; 1484 1485 // Create a new quiz with one attempt started. 1486 list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true); 1487 1488 $this->setUser($this->student); 1489 1490 // Invalid attempt, invalid id. 1491 try { 1492 $params = array('attemptid' => -1); 1493 testable_mod_quiz_external::validate_attempt_review($params); 1494 $this->fail('Exception expected due invalid id.'); 1495 } catch (\dml_missing_record_exception $e) { 1496 $this->assertEquals('invalidrecord', $e->errorcode); 1497 } 1498 1499 // Invalid attempt, not closed. 1500 try { 1501 $params = array('attemptid' => $attempt->id); 1502 testable_mod_quiz_external::validate_attempt_review($params); 1503 $this->fail('Exception expected due not closed attempt.'); 1504 } catch (\moodle_quiz_exception $e) { 1505 $this->assertEquals('attemptclosed', $e->errorcode); 1506 } 1507 1508 // Test ok case (finished attempt). 1509 list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true, true); 1510 1511 $params = array('attemptid' => $attempt->id); 1512 testable_mod_quiz_external::validate_attempt_review($params); 1513 1514 // Teacher should be able to view the review of one student's attempt. 1515 $this->setUser($this->teacher); 1516 testable_mod_quiz_external::validate_attempt_review($params); 1517 1518 // We should not see other students attempts. 1519 $anotherstudent = self::getDataGenerator()->create_user(); 1520 $this->getDataGenerator()->enrol_user($anotherstudent->id, $this->course->id, $this->studentrole->id, 'manual'); 1521 1522 $this->setUser($anotherstudent); 1523 try { 1524 $params = array('attemptid' => $attempt->id); 1525 testable_mod_quiz_external::validate_attempt_review($params); 1526 $this->fail('Exception expected due missing permissions.'); 1527 } catch (\moodle_quiz_exception $e) { 1528 $this->assertEquals('noreviewattempt', $e->errorcode); 1529 } 1530 } 1531 1532 1533 /** 1534 * Test get_attempt_review 1535 */ 1536 public function test_get_attempt_review() { 1537 global $DB; 1538 1539 // Create a new quiz with two questions and one attempt finished. 1540 list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true, true); 1541 1542 // Add feedback to the quiz. 1543 $feedback = new \stdClass(); 1544 $feedback->quizid = $quiz->id; 1545 $feedback->feedbacktext = 'Feedback text 1'; 1546 $feedback->feedbacktextformat = 1; 1547 $feedback->mingrade = 49; 1548 $feedback->maxgrade = 100; 1549 $feedback->id = $DB->insert_record('quiz_feedback', $feedback); 1550 1551 $feedback->feedbacktext = 'Feedback text 2'; 1552 $feedback->feedbacktextformat = 1; 1553 $feedback->mingrade = 30; 1554 $feedback->maxgrade = 48; 1555 $feedback->id = $DB->insert_record('quiz_feedback', $feedback); 1556 1557 $result = mod_quiz_external::get_attempt_review($attempt->id); 1558 $result = \external_api::clean_returnvalue(mod_quiz_external::get_attempt_review_returns(), $result); 1559 1560 // Two questions, one completed and correct, the other gave up. 1561 $this->assertEquals(50, $result['grade']); 1562 $this->assertEquals(1, $result['attempt']['attempt']); 1563 $this->assertEquals('finished', $result['attempt']['state']); 1564 $this->assertEquals(1, $result['attempt']['sumgrades']); 1565 $this->assertCount(2, $result['questions']); 1566 $this->assertEquals('gradedright', $result['questions'][0]['state']); 1567 $this->assertEquals(1, $result['questions'][0]['slot']); 1568 $this->assertEquals('gaveup', $result['questions'][1]['state']); 1569 $this->assertEquals(2, $result['questions'][1]['slot']); 1570 1571 $this->assertCount(1, $result['additionaldata']); 1572 $this->assertEquals('feedback', $result['additionaldata'][0]['id']); 1573 $this->assertEquals('Feedback', $result['additionaldata'][0]['title']); 1574 $this->assertEquals('Feedback text 1', $result['additionaldata'][0]['content']); 1575 1576 // Only first page. 1577 $result = mod_quiz_external::get_attempt_review($attempt->id, 0); 1578 $result = \external_api::clean_returnvalue(mod_quiz_external::get_attempt_review_returns(), $result); 1579 1580 $this->assertEquals(50, $result['grade']); 1581 $this->assertEquals(1, $result['attempt']['attempt']); 1582 $this->assertEquals('finished', $result['attempt']['state']); 1583 $this->assertEquals(1, $result['attempt']['sumgrades']); 1584 $this->assertCount(1, $result['questions']); 1585 $this->assertEquals('gradedright', $result['questions'][0]['state']); 1586 $this->assertEquals(1, $result['questions'][0]['slot']); 1587 1588 $this->assertCount(1, $result['additionaldata']); 1589 $this->assertEquals('feedback', $result['additionaldata'][0]['id']); 1590 $this->assertEquals('Feedback', $result['additionaldata'][0]['title']); 1591 $this->assertEquals('Feedback text 1', $result['additionaldata'][0]['content']); 1592 1593 } 1594 1595 /** 1596 * Test test_view_attempt 1597 */ 1598 public function test_view_attempt() { 1599 global $DB; 1600 1601 // Create a new quiz with two questions and one attempt started. 1602 list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true, false); 1603 1604 // Test user with full capabilities. 1605 $this->setUser($this->student); 1606 1607 // Trigger and capture the event. 1608 $sink = $this->redirectEvents(); 1609 1610 $result = mod_quiz_external::view_attempt($attempt->id, 0); 1611 $result = \external_api::clean_returnvalue(mod_quiz_external::view_attempt_returns(), $result); 1612 $this->assertTrue($result['status']); 1613 1614 $events = $sink->get_events(); 1615 $this->assertCount(1, $events); 1616 $event = array_shift($events); 1617 1618 // Checking that the event contains the expected values. 1619 $this->assertInstanceOf('\mod_quiz\event\attempt_viewed', $event); 1620 $this->assertEquals($context, $event->get_context()); 1621 $this->assertEventContextNotUsed($event); 1622 $this->assertNotEmpty($event->get_name()); 1623 1624 // Now, force the quiz with QUIZ_NAVMETHOD_SEQ (sequencial) navigation method. 1625 $DB->set_field('quiz', 'navmethod', QUIZ_NAVMETHOD_SEQ, array('id' => $quiz->id)); 1626 // Quiz requiring preflightdata. 1627 $DB->set_field('quiz', 'password', 'abcdef', array('id' => $quiz->id)); 1628 $preflightdata = array(array("name" => "quizpassword", "value" => 'abcdef')); 1629 1630 // See next page. 1631 $result = mod_quiz_external::view_attempt($attempt->id, 1, $preflightdata); 1632 $result = \external_api::clean_returnvalue(mod_quiz_external::view_attempt_returns(), $result); 1633 $this->assertTrue($result['status']); 1634 1635 $events = $sink->get_events(); 1636 $this->assertCount(2, $events); 1637 1638 // Try to go to previous page. 1639 try { 1640 mod_quiz_external::view_attempt($attempt->id, 0); 1641 $this->fail('Exception expected due to try to see a previous page.'); 1642 } catch (\moodle_quiz_exception $e) { 1643 $this->assertEquals('Out of sequence access', $e->errorcode); 1644 } 1645 1646 } 1647 1648 /** 1649 * Test test_view_attempt_summary 1650 */ 1651 public function test_view_attempt_summary() { 1652 global $DB; 1653 1654 // Create a new quiz with two questions and one attempt started. 1655 list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true, false); 1656 1657 // Test user with full capabilities. 1658 $this->setUser($this->student); 1659 1660 // Trigger and capture the event. 1661 $sink = $this->redirectEvents(); 1662 1663 $result = mod_quiz_external::view_attempt_summary($attempt->id); 1664 $result = \external_api::clean_returnvalue(mod_quiz_external::view_attempt_summary_returns(), $result); 1665 $this->assertTrue($result['status']); 1666 1667 $events = $sink->get_events(); 1668 $this->assertCount(1, $events); 1669 $event = array_shift($events); 1670 1671 // Checking that the event contains the expected values. 1672 $this->assertInstanceOf('\mod_quiz\event\attempt_summary_viewed', $event); 1673 $this->assertEquals($context, $event->get_context()); 1674 $moodlequiz = new \moodle_url('/mod/quiz/summary.php', array('attempt' => $attempt->id)); 1675 $this->assertEquals($moodlequiz, $event->get_url()); 1676 $this->assertEventContextNotUsed($event); 1677 $this->assertNotEmpty($event->get_name()); 1678 1679 // Quiz requiring preflightdata. 1680 $DB->set_field('quiz', 'password', 'abcdef', array('id' => $quiz->id)); 1681 $preflightdata = array(array("name" => "quizpassword", "value" => 'abcdef')); 1682 1683 $result = mod_quiz_external::view_attempt_summary($attempt->id, $preflightdata); 1684 $result = \external_api::clean_returnvalue(mod_quiz_external::view_attempt_summary_returns(), $result); 1685 $this->assertTrue($result['status']); 1686 1687 } 1688 1689 /** 1690 * Test test_view_attempt_summary 1691 */ 1692 public function test_view_attempt_review() { 1693 global $DB; 1694 1695 // Create a new quiz with two questions and one attempt finished. 1696 list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true, true); 1697 1698 // Test user with full capabilities. 1699 $this->setUser($this->student); 1700 1701 // Trigger and capture the event. 1702 $sink = $this->redirectEvents(); 1703 1704 $result = mod_quiz_external::view_attempt_review($attempt->id, 0); 1705 $result = \external_api::clean_returnvalue(mod_quiz_external::view_attempt_review_returns(), $result); 1706 $this->assertTrue($result['status']); 1707 1708 $events = $sink->get_events(); 1709 $this->assertCount(1, $events); 1710 $event = array_shift($events); 1711 1712 // Checking that the event contains the expected values. 1713 $this->assertInstanceOf('\mod_quiz\event\attempt_reviewed', $event); 1714 $this->assertEquals($context, $event->get_context()); 1715 $moodlequiz = new \moodle_url('/mod/quiz/review.php', array('attempt' => $attempt->id)); 1716 $this->assertEquals($moodlequiz, $event->get_url()); 1717 $this->assertEventContextNotUsed($event); 1718 $this->assertNotEmpty($event->get_name()); 1719 1720 } 1721 1722 /** 1723 * Test get_quiz_feedback_for_grade 1724 */ 1725 public function test_get_quiz_feedback_for_grade() { 1726 global $DB; 1727 1728 // Add feedback to the quiz. 1729 $feedback = new \stdClass(); 1730 $feedback->quizid = $this->quiz->id; 1731 $feedback->feedbacktext = 'Feedback text 1'; 1732 $feedback->feedbacktextformat = 1; 1733 $feedback->mingrade = 49; 1734 $feedback->maxgrade = 100; 1735 $feedback->id = $DB->insert_record('quiz_feedback', $feedback); 1736 // Add a fake inline image to the feedback text. 1737 $filename = 'shouldbeanimage.jpg'; 1738 $filerecordinline = array( 1739 'contextid' => $this->context->id, 1740 'component' => 'mod_quiz', 1741 'filearea' => 'feedback', 1742 'itemid' => $feedback->id, 1743 'filepath' => '/', 1744 'filename' => $filename, 1745 ); 1746 $fs = get_file_storage(); 1747 $fs->create_file_from_string($filerecordinline, 'image contents (not really)'); 1748 1749 $feedback->feedbacktext = 'Feedback text 2'; 1750 $feedback->feedbacktextformat = 1; 1751 $feedback->mingrade = 30; 1752 $feedback->maxgrade = 49; 1753 $feedback->id = $DB->insert_record('quiz_feedback', $feedback); 1754 1755 $result = mod_quiz_external::get_quiz_feedback_for_grade($this->quiz->id, 50); 1756 $result = \external_api::clean_returnvalue(mod_quiz_external::get_quiz_feedback_for_grade_returns(), $result); 1757 $this->assertEquals('Feedback text 1', $result['feedbacktext']); 1758 $this->assertEquals($filename, $result['feedbackinlinefiles'][0]['filename']); 1759 $this->assertEquals(FORMAT_HTML, $result['feedbacktextformat']); 1760 1761 $result = mod_quiz_external::get_quiz_feedback_for_grade($this->quiz->id, 30); 1762 $result = \external_api::clean_returnvalue(mod_quiz_external::get_quiz_feedback_for_grade_returns(), $result); 1763 $this->assertEquals('Feedback text 2', $result['feedbacktext']); 1764 $this->assertEquals(FORMAT_HTML, $result['feedbacktextformat']); 1765 1766 $result = mod_quiz_external::get_quiz_feedback_for_grade($this->quiz->id, 10); 1767 $result = \external_api::clean_returnvalue(mod_quiz_external::get_quiz_feedback_for_grade_returns(), $result); 1768 $this->assertEquals('', $result['feedbacktext']); 1769 $this->assertEquals(FORMAT_MOODLE, $result['feedbacktextformat']); 1770 } 1771 1772 /** 1773 * Test get_quiz_access_information 1774 */ 1775 public function test_get_quiz_access_information() { 1776 global $DB; 1777 1778 // Create a new quiz. 1779 $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); 1780 $data = array('course' => $this->course->id); 1781 $quiz = $quizgenerator->create_instance($data); 1782 1783 $this->setUser($this->student); 1784 1785 // Default restrictions (none). 1786 $result = mod_quiz_external::get_quiz_access_information($quiz->id); 1787 $result = \external_api::clean_returnvalue(mod_quiz_external::get_quiz_access_information_returns(), $result); 1788 1789 $expected = array( 1790 'canattempt' => true, 1791 'canmanage' => false, 1792 'canpreview' => false, 1793 'canreviewmyattempts' => true, 1794 'canviewreports' => false, 1795 'accessrules' => [], 1796 // This rule is always used, even if the quiz has no open or close date. 1797 'activerulenames' => ['quizaccess_openclosedate'], 1798 'preventaccessreasons' => [], 1799 'warnings' => [] 1800 ); 1801 1802 $this->assertEquals($expected, $result); 1803 1804 // Now teacher, different privileges. 1805 $this->setUser($this->teacher); 1806 $result = mod_quiz_external::get_quiz_access_information($quiz->id); 1807 $result = \external_api::clean_returnvalue(mod_quiz_external::get_quiz_access_information_returns(), $result); 1808 1809 $expected['canmanage'] = true; 1810 $expected['canpreview'] = true; 1811 $expected['canviewreports'] = true; 1812 $expected['canattempt'] = false; 1813 $expected['canreviewmyattempts'] = false; 1814 1815 $this->assertEquals($expected, $result); 1816 1817 $this->setUser($this->student); 1818 // Now add some restrictions. 1819 $quiz->timeopen = time() + DAYSECS; 1820 $quiz->timeclose = time() + WEEKSECS; 1821 $quiz->password = '123456'; 1822 $DB->update_record('quiz', $quiz); 1823 1824 $result = mod_quiz_external::get_quiz_access_information($quiz->id); 1825 $result = \external_api::clean_returnvalue(mod_quiz_external::get_quiz_access_information_returns(), $result); 1826 1827 // Access is limited by time and password, but only the password limit has a description. 1828 $this->assertCount(1, $result['accessrules']); 1829 // Two rule names, password and open/close date. 1830 $this->assertCount(2, $result['activerulenames']); 1831 $this->assertCount(1, $result['preventaccessreasons']); 1832 1833 } 1834 1835 /** 1836 * Test get_attempt_access_information 1837 */ 1838 public function test_get_attempt_access_information() { 1839 global $DB; 1840 1841 $this->setAdminUser(); 1842 1843 // Create a new quiz with attempts. 1844 $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); 1845 $data = array('course' => $this->course->id, 1846 'sumgrades' => 2); 1847 $quiz = $quizgenerator->create_instance($data); 1848 1849 // Create some questions. 1850 $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); 1851 1852 $cat = $questiongenerator->create_question_category(); 1853 $question = $questiongenerator->create_question('numerical', null, array('category' => $cat->id)); 1854 quiz_add_quiz_question($question->id, $quiz); 1855 1856 $question = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id)); 1857 quiz_add_quiz_question($question->id, $quiz); 1858 1859 // Add new question types in the category (for the random one). 1860 $question = $questiongenerator->create_question('truefalse', null, array('category' => $cat->id)); 1861 $question = $questiongenerator->create_question('essay', null, array('category' => $cat->id)); 1862 1863 quiz_add_random_questions($quiz, 0, $cat->id, 1, false); 1864 1865 $quizobj = quiz::create($quiz->id, $this->student->id); 1866 1867 // Set grade to pass. 1868 $item = \grade_item::fetch(array('courseid' => $this->course->id, 'itemtype' => 'mod', 1869 'itemmodule' => 'quiz', 'iteminstance' => $quiz->id, 'outcomeid' => null)); 1870 $item->gradepass = 80; 1871 $item->update(); 1872 1873 $this->setUser($this->student); 1874 1875 // Default restrictions (none). 1876 $result = mod_quiz_external::get_attempt_access_information($quiz->id); 1877 $result = \external_api::clean_returnvalue(mod_quiz_external::get_attempt_access_information_returns(), $result); 1878 1879 $expected = array( 1880 'isfinished' => false, 1881 'preventnewattemptreasons' => [], 1882 'warnings' => [] 1883 ); 1884 1885 $this->assertEquals($expected, $result); 1886 1887 // Limited attempts. 1888 $quiz->attempts = 1; 1889 $DB->update_record('quiz', $quiz); 1890 1891 // Now, do one attempt. 1892 $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); 1893 $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); 1894 1895 $timenow = time(); 1896 $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $this->student->id); 1897 quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow); 1898 quiz_attempt_save_started($quizobj, $quba, $attempt); 1899 1900 // Process some responses from the student. 1901 $attemptobj = quiz_attempt::create($attempt->id); 1902 $tosubmit = array(1 => array('answer' => '3.14')); 1903 $attemptobj->process_submitted_actions($timenow, false, $tosubmit); 1904 1905 // Finish the attempt. 1906 $attemptobj = quiz_attempt::create($attempt->id); 1907 $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question()); 1908 $attemptobj->process_finish($timenow, false); 1909 1910 // Can we start a new attempt? We shall not! 1911 $result = mod_quiz_external::get_attempt_access_information($quiz->id, $attempt->id); 1912 $result = \external_api::clean_returnvalue(mod_quiz_external::get_attempt_access_information_returns(), $result); 1913 1914 // Now new attemps allowed. 1915 $this->assertCount(1, $result['preventnewattemptreasons']); 1916 $this->assertFalse($result['ispreflightcheckrequired']); 1917 $this->assertEquals(get_string('nomoreattempts', 'quiz'), $result['preventnewattemptreasons'][0]); 1918 1919 } 1920 1921 /** 1922 * Test get_quiz_required_qtypes 1923 */ 1924 public function test_get_quiz_required_qtypes() { 1925 $this->setAdminUser(); 1926 1927 // Create a new quiz. 1928 $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); 1929 $data = array('course' => $this->course->id); 1930 $quiz = $quizgenerator->create_instance($data); 1931 1932 // Create some questions. 1933 $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); 1934 1935 $cat = $questiongenerator->create_question_category(); 1936 $question = $questiongenerator->create_question('numerical', null, array('category' => $cat->id)); 1937 quiz_add_quiz_question($question->id, $quiz); 1938 1939 $question = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id)); 1940 quiz_add_quiz_question($question->id, $quiz); 1941 1942 // Add new question types in the category (for the random one). 1943 $question = $questiongenerator->create_question('truefalse', null, array('category' => $cat->id)); 1944 $question = $questiongenerator->create_question('essay', null, array('category' => $cat->id)); 1945 1946 quiz_add_random_questions($quiz, 0, $cat->id, 1, false); 1947 1948 $this->setUser($this->student); 1949 1950 $result = mod_quiz_external::get_quiz_required_qtypes($quiz->id); 1951 $result = \external_api::clean_returnvalue(mod_quiz_external::get_quiz_required_qtypes_returns(), $result); 1952 1953 $expected = array( 1954 'questiontypes' => ['essay', 'numerical', 'random', 'shortanswer', 'truefalse'], 1955 'warnings' => [] 1956 ); 1957 1958 $this->assertEquals($expected, $result); 1959 1960 } 1961 1962 /** 1963 * Test that a sequential navigation quiz is not allowing to see questions in advance except if reviewing 1964 */ 1965 public function test_sequential_navigation_view_attempt() { 1966 // Test user with full capabilities. 1967 $quiz = $this->prepare_sequential_quiz(); 1968 $attemptobj = $this->create_quiz_attempt_object($quiz); 1969 $this->setUser($this->student); 1970 // Check out of sequence access for view. 1971 $this->assertNotEmpty(mod_quiz_external::view_attempt($attemptobj->get_attemptid(), 0, [])); 1972 try { 1973 mod_quiz_external::view_attempt($attemptobj->get_attemptid(), 3, []); 1974 $this->fail('Exception expected due to out of sequence access.'); 1975 } catch (\moodle_exception $e) { 1976 $this->assertStringContainsString('quiz/Out of sequence access', $e->getMessage()); 1977 } 1978 } 1979 1980 /** 1981 * Test that a sequential navigation quiz is not allowing to see questions in advance for a student 1982 */ 1983 public function test_sequential_navigation_attempt_summary() { 1984 // Test user with full capabilities. 1985 $quiz = $this->prepare_sequential_quiz(); 1986 $attemptobj = $this->create_quiz_attempt_object($quiz); 1987 $this->setUser($this->student); 1988 // Check that we do not return other questions than the one currently viewed. 1989 $result = mod_quiz_external::get_attempt_summary($attemptobj->get_attemptid()); 1990 $this->assertCount(1, $result['questions']); 1991 $this->assertStringContainsString('Question (1)', $result['questions'][0]['html']); 1992 } 1993 1994 /** 1995 * Test that a sequential navigation quiz is not allowing to see questions in advance for student 1996 */ 1997 public function test_sequential_navigation_get_attempt_data() { 1998 // Test user with full capabilities. 1999 $quiz = $this->prepare_sequential_quiz(); 2000 $attemptobj = $this->create_quiz_attempt_object($quiz); 2001 $this->setUser($this->student); 2002 // Test invalid instance id. 2003 try { 2004 mod_quiz_external::get_attempt_data($attemptobj->get_attemptid(), 2); 2005 $this->fail('Exception expected due to out of sequence access.'); 2006 } catch (\moodle_exception $e) { 2007 $this->assertStringContainsString('quiz/Out of sequence access', $e->getMessage()); 2008 } 2009 // Now we moved to page 1, we should see page 2 and 1 but not 0 or 3. 2010 $attemptobj->set_currentpage(1); 2011 // Test invalid instance id. 2012 try { 2013 mod_quiz_external::get_attempt_data($attemptobj->get_attemptid(), 0); 2014 $this->fail('Exception expected due to out of sequence access.'); 2015 } catch (\moodle_exception $e) { 2016 $this->assertStringContainsString('quiz/Out of sequence access', $e->getMessage()); 2017 } 2018 2019 try { 2020 mod_quiz_external::get_attempt_data($attemptobj->get_attemptid(), 3); 2021 $this->fail('Exception expected due to out of sequence access.'); 2022 } catch (\moodle_exception $e) { 2023 $this->assertStringContainsString('quiz/Out of sequence access', $e->getMessage()); 2024 } 2025 2026 // Now we can see page 1. 2027 $result = mod_quiz_external::get_attempt_data($attemptobj->get_attemptid(), 1); 2028 $this->assertCount(1, $result['questions']); 2029 $this->assertStringContainsString('Question (2)', $result['questions'][0]['html']); 2030 } 2031 2032 /** 2033 * Prepare quiz for sequential navigation tests 2034 * 2035 * @return quiz 2036 */ 2037 private function prepare_sequential_quiz() { 2038 // Create a new quiz with 5 questions and one attempt started. 2039 // Create a new quiz with attempts. 2040 $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); 2041 $data = [ 2042 'course' => $this->course->id, 2043 'sumgrades' => 2, 2044 'preferredbehaviour' => 'deferredfeedback', 2045 'navmethod' => QUIZ_NAVMETHOD_SEQ 2046 ]; 2047 $quiz = $quizgenerator->create_instance($data); 2048 2049 // Now generate the questions. 2050 $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); 2051 $cat = $questiongenerator->create_question_category(); 2052 for ($pageindex = 1; $pageindex <= 5; $pageindex++) { 2053 $question = $questiongenerator->create_question('truefalse', null, [ 2054 'category' => $cat->id, 2055 'questiontext' => ['text' => "Question ($pageindex)"] 2056 ]); 2057 quiz_add_quiz_question($question->id, $quiz, $pageindex); 2058 } 2059 2060 $quizobj = quiz::create($quiz->id, $this->student->id); 2061 // Set grade to pass. 2062 $item = \grade_item::fetch(array('courseid' => $this->course->id, 'itemtype' => 'mod', 2063 'itemmodule' => 'quiz', 'iteminstance' => $quiz->id, 'outcomeid' => null)); 2064 $item->gradepass = 80; 2065 $item->update(); 2066 return $quizobj; 2067 } 2068 2069 /** 2070 * Create question attempt 2071 * 2072 * @param quiz $quizobj 2073 * @param int|null $userid 2074 * @param bool|null $ispreview 2075 * @return quiz_attempt 2076 * @throws \moodle_exception 2077 */ 2078 private function create_quiz_attempt_object($quizobj, $userid = null, $ispreview = false) { 2079 global $USER; 2080 $timenow = time(); 2081 // Now, do one attempt. 2082 $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); 2083 $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); 2084 $attemptnumber = 1; 2085 if (!empty($USER->id)) { 2086 $attemptnumber = count(quiz_get_user_attempts($quizobj->get_quizid(), $USER->id)) + 1; 2087 } 2088 $attempt = quiz_create_attempt($quizobj, $attemptnumber, false, $timenow, $ispreview, $userid ?? $this->student->id); 2089 quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $timenow); 2090 quiz_attempt_save_started($quizobj, $quba, $attempt); 2091 $attemptobj = quiz_attempt::create($attempt->id); 2092 return $attemptobj; 2093 } 2094 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body