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