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