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