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