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