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(): void { 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 // Check question options. 1182 $this->assertNotEmpty(5, $result['questions'][0]['settings']); 1183 // Check at least some settings returned. 1184 $this->assertCount(4, (array) json_decode($result['questions'][0]['settings'])); 1185 1186 // Submit a response for the first question. 1187 $tosubmit = array(1 => array('answer' => '3.14')); 1188 $attemptobj->process_submitted_actions(time(), false, $tosubmit); 1189 $result = mod_quiz_external::get_attempt_summary($attempt->id); 1190 $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result); 1191 1192 // Check it's marked as completed only the first one. 1193 $this->assertEquals('complete', $result['questions'][0]['state']); 1194 $this->assertEquals('todo', $result['questions'][1]['state']); 1195 $this->assertEquals(1, $result['questions'][0]['number']); 1196 $this->assertEquals(2, $result['questions'][1]['number']); 1197 $this->assertFalse($result['questions'][0]['flagged']); 1198 $this->assertFalse($result['questions'][1]['flagged']); 1199 $this->assertEmpty($result['questions'][0]['mark']); 1200 $this->assertEmpty($result['questions'][1]['mark']); 1201 $this->assertEquals(2, $result['questions'][0]['sequencecheck']); 1202 $this->assertEquals(1, $result['questions'][1]['sequencecheck']); 1203 $this->assertGreaterThanOrEqual($timenow, $result['questions'][0]['lastactiontime']); 1204 $this->assertGreaterThanOrEqual($timenow, $result['questions'][1]['lastactiontime']); 1205 $this->assertEquals(false, $result['questions'][0]['hasautosavedstep']); 1206 $this->assertEquals(false, $result['questions'][1]['hasautosavedstep']); 1207 1208 } 1209 1210 /** 1211 * Test save_attempt 1212 */ 1213 public function test_save_attempt() { 1214 1215 $timenow = time(); 1216 // Create a new quiz with one attempt started. 1217 list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true); 1218 1219 // Response for slot 1. 1220 $prefix = $quba->get_field_prefix(1); 1221 $data = array( 1222 array('name' => 'slots', 'value' => 1), 1223 array('name' => $prefix . ':sequencecheck', 1224 'value' => $attemptobj->get_question_attempt(1)->get_sequence_check_count()), 1225 array('name' => $prefix . 'answer', 'value' => 1), 1226 ); 1227 1228 $this->setUser($this->student); 1229 1230 $result = mod_quiz_external::save_attempt($attempt->id, $data); 1231 $result = external_api::clean_returnvalue(mod_quiz_external::save_attempt_returns(), $result); 1232 $this->assertTrue($result['status']); 1233 1234 // Now, get the summary. 1235 $result = mod_quiz_external::get_attempt_summary($attempt->id); 1236 $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result); 1237 1238 // Check it's marked as completed only the first one. 1239 $this->assertEquals('complete', $result['questions'][0]['state']); 1240 $this->assertEquals('todo', $result['questions'][1]['state']); 1241 $this->assertEquals(1, $result['questions'][0]['number']); 1242 $this->assertEquals(2, $result['questions'][1]['number']); 1243 $this->assertFalse($result['questions'][0]['flagged']); 1244 $this->assertFalse($result['questions'][1]['flagged']); 1245 $this->assertEmpty($result['questions'][0]['mark']); 1246 $this->assertEmpty($result['questions'][1]['mark']); 1247 $this->assertEquals(1, $result['questions'][0]['sequencecheck']); 1248 $this->assertEquals(1, $result['questions'][1]['sequencecheck']); 1249 $this->assertGreaterThanOrEqual($timenow, $result['questions'][0]['lastactiontime']); 1250 $this->assertGreaterThanOrEqual($timenow, $result['questions'][1]['lastactiontime']); 1251 $this->assertEquals(true, $result['questions'][0]['hasautosavedstep']); 1252 $this->assertEquals(false, $result['questions'][1]['hasautosavedstep']); 1253 1254 // Now, second slot. 1255 $prefix = $quba->get_field_prefix(2); 1256 $data = array( 1257 array('name' => 'slots', 'value' => 2), 1258 array('name' => $prefix . ':sequencecheck', 1259 'value' => $attemptobj->get_question_attempt(1)->get_sequence_check_count()), 1260 array('name' => $prefix . 'answer', 'value' => 1), 1261 ); 1262 1263 $result = mod_quiz_external::save_attempt($attempt->id, $data); 1264 $result = external_api::clean_returnvalue(mod_quiz_external::save_attempt_returns(), $result); 1265 $this->assertTrue($result['status']); 1266 1267 // Now, get the summary. 1268 $result = mod_quiz_external::get_attempt_summary($attempt->id); 1269 $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result); 1270 1271 // Check it's marked as completed only the first one. 1272 $this->assertEquals('complete', $result['questions'][0]['state']); 1273 $this->assertEquals(1, $result['questions'][0]['sequencecheck']); 1274 $this->assertEquals('complete', $result['questions'][1]['state']); 1275 $this->assertEquals(1, $result['questions'][1]['sequencecheck']); 1276 1277 } 1278 1279 /** 1280 * Test process_attempt 1281 */ 1282 public function test_process_attempt() { 1283 global $DB; 1284 1285 $timenow = time(); 1286 // Create a new quiz with three questions and one attempt started. 1287 list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true, false, 1288 'deferredfeedback', true); 1289 1290 // Response for slot 1. 1291 $prefix = $quba->get_field_prefix(1); 1292 $data = array( 1293 array('name' => 'slots', 'value' => 1), 1294 array('name' => $prefix . ':sequencecheck', 1295 'value' => $attemptobj->get_question_attempt(1)->get_sequence_check_count()), 1296 array('name' => $prefix . 'answer', 'value' => 1), 1297 ); 1298 1299 $this->setUser($this->student); 1300 1301 $result = mod_quiz_external::process_attempt($attempt->id, $data); 1302 $result = external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result); 1303 $this->assertEquals(quiz_attempt::IN_PROGRESS, $result['state']); 1304 1305 $result = mod_quiz_external::get_attempt_data($attempt->id, 2); 1306 1307 // Now, get the summary. 1308 $result = mod_quiz_external::get_attempt_summary($attempt->id); 1309 $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result); 1310 $this->assertDebuggingCalled(); // Expect $PAGE->set_url debugging. 1311 1312 // Check it's marked as completed only the first one. 1313 $this->assertEquals('complete', $result['questions'][0]['state']); 1314 $this->assertEquals('todo', $result['questions'][1]['state']); 1315 $this->assertEquals(1, $result['questions'][0]['number']); 1316 $this->assertEquals(2, $result['questions'][1]['number']); 1317 $this->assertFalse($result['questions'][0]['flagged']); 1318 $this->assertFalse($result['questions'][1]['flagged']); 1319 $this->assertEmpty($result['questions'][0]['mark']); 1320 $this->assertEmpty($result['questions'][1]['mark']); 1321 $this->assertEquals(2, $result['questions'][0]['sequencecheck']); 1322 $this->assertEquals(2, $result['questions'][0]['sequencecheck']); 1323 $this->assertGreaterThanOrEqual($timenow, $result['questions'][0]['lastactiontime']); 1324 $this->assertGreaterThanOrEqual($timenow, $result['questions'][0]['lastactiontime']); 1325 $this->assertEquals(false, $result['questions'][0]['hasautosavedstep']); 1326 $this->assertEquals(false, $result['questions'][0]['hasautosavedstep']); 1327 1328 // Now, second slot. 1329 $prefix = $quba->get_field_prefix(2); 1330 $data = array( 1331 array('name' => 'slots', 'value' => 2), 1332 array('name' => $prefix . ':sequencecheck', 1333 'value' => $attemptobj->get_question_attempt(1)->get_sequence_check_count()), 1334 array('name' => $prefix . 'answer', 'value' => 1), 1335 array('name' => $prefix . ':flagged', 'value' => 1), 1336 ); 1337 1338 $result = mod_quiz_external::process_attempt($attempt->id, $data); 1339 $result = external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result); 1340 $this->assertEquals(quiz_attempt::IN_PROGRESS, $result['state']); 1341 1342 // Now, get the summary. 1343 $result = mod_quiz_external::get_attempt_summary($attempt->id); 1344 $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result); 1345 1346 // Check it's marked as completed the two first questions. 1347 $this->assertEquals('complete', $result['questions'][0]['state']); 1348 $this->assertEquals('complete', $result['questions'][1]['state']); 1349 $this->assertFalse($result['questions'][0]['flagged']); 1350 $this->assertTrue($result['questions'][1]['flagged']); 1351 1352 // Add files in the attachment response. 1353 $draftitemid = file_get_unused_draft_itemid(); 1354 $filerecordinline = array( 1355 'contextid' => context_user::instance($this->student->id)->id, 1356 'component' => 'user', 1357 'filearea' => 'draft', 1358 'itemid' => $draftitemid, 1359 'filepath' => '/', 1360 'filename' => 'faketxt.txt', 1361 ); 1362 $fs = get_file_storage(); 1363 $fs->create_file_from_string($filerecordinline, 'fake txt contents 1.'); 1364 1365 // Last slot. 1366 $prefix = $quba->get_field_prefix(3); 1367 $data = array( 1368 array('name' => 'slots', 'value' => 3), 1369 array('name' => $prefix . ':sequencecheck', 1370 'value' => $attemptobj->get_question_attempt(1)->get_sequence_check_count()), 1371 array('name' => $prefix . 'answer', 'value' => 'Some test'), 1372 array('name' => $prefix . 'answerformat', 'value' => FORMAT_HTML), 1373 array('name' => $prefix . 'attachments', 'value' => $draftitemid), 1374 ); 1375 1376 $result = mod_quiz_external::process_attempt($attempt->id, $data); 1377 $result = external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result); 1378 $this->assertEquals(quiz_attempt::IN_PROGRESS, $result['state']); 1379 1380 // Now, get the summary. 1381 $result = mod_quiz_external::get_attempt_summary($attempt->id); 1382 $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result); 1383 1384 $this->assertEquals('complete', $result['questions'][0]['state']); 1385 $this->assertEquals('complete', $result['questions'][1]['state']); 1386 $this->assertEquals('complete', $result['questions'][2]['state']); 1387 $this->assertFalse($result['questions'][0]['flagged']); 1388 $this->assertTrue($result['questions'][1]['flagged']); 1389 $this->assertFalse($result['questions'][2]['flagged']); 1390 1391 // Check submitted files are there. 1392 $this->assertCount(1, $result['questions'][2]['responsefileareas']); 1393 $this->assertEquals('attachments', $result['questions'][2]['responsefileareas'][0]['area']); 1394 $this->assertCount(1, $result['questions'][2]['responsefileareas'][0]['files']); 1395 $this->assertEquals($filerecordinline['filename'], $result['questions'][2]['responsefileareas'][0]['files'][0]['filename']); 1396 1397 // Finish the attempt. 1398 $sink = $this->redirectMessages(); 1399 $result = mod_quiz_external::process_attempt($attempt->id, array(), true); 1400 $result = external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result); 1401 $this->assertEquals(quiz_attempt::FINISHED, $result['state']); 1402 $messages = $sink->get_messages(); 1403 $message = reset($messages); 1404 $sink->close(); 1405 // Test customdata. 1406 if (!empty($message->customdata)) { 1407 $customdata = json_decode($message->customdata); 1408 $this->assertEquals($quizobj->get_quizid(), $customdata->instance); 1409 $this->assertEquals($quizobj->get_cmid(), $customdata->cmid); 1410 $this->assertEquals($attempt->id, $customdata->attemptid); 1411 $this->assertObjectHasAttribute('notificationiconurl', $customdata); 1412 } 1413 1414 // Start new attempt. 1415 $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); 1416 $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); 1417 1418 $timenow = time(); 1419 $attempt = quiz_create_attempt($quizobj, 2, false, $timenow, false, $this->student->id); 1420 quiz_start_new_attempt($quizobj, $quba, $attempt, 2, $timenow); 1421 quiz_attempt_save_started($quizobj, $quba, $attempt); 1422 1423 // Force grace period, attempt going to overdue. 1424 $quiz->timeclose = $timenow - 10; 1425 $quiz->graceperiod = 60; 1426 $quiz->overduehandling = 'graceperiod'; 1427 $DB->update_record('quiz', $quiz); 1428 1429 $result = mod_quiz_external::process_attempt($attempt->id, array()); 1430 $result = external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result); 1431 $this->assertEquals(quiz_attempt::OVERDUE, $result['state']); 1432 1433 // Force grace period for time limit. 1434 $quiz->timeclose = 0; 1435 $quiz->timelimit = 1; 1436 $quiz->graceperiod = 60; 1437 $quiz->overduehandling = 'graceperiod'; 1438 $DB->update_record('quiz', $quiz); 1439 1440 $timenow = time(); 1441 $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); 1442 $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); 1443 $attempt = quiz_create_attempt($quizobj, 3, 2, $timenow - 10, false, $this->student->id); 1444 quiz_start_new_attempt($quizobj, $quba, $attempt, 2, $timenow - 10); 1445 quiz_attempt_save_started($quizobj, $quba, $attempt); 1446 1447 $result = mod_quiz_external::process_attempt($attempt->id, array()); 1448 $result = external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result); 1449 $this->assertEquals(quiz_attempt::OVERDUE, $result['state']); 1450 1451 // New attempt. 1452 $timenow = time(); 1453 $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); 1454 $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); 1455 $attempt = quiz_create_attempt($quizobj, 4, 3, $timenow, false, $this->student->id); 1456 quiz_start_new_attempt($quizobj, $quba, $attempt, 3, $timenow); 1457 quiz_attempt_save_started($quizobj, $quba, $attempt); 1458 1459 // Force abandon. 1460 $quiz->timeclose = $timenow - HOURSECS; 1461 $DB->update_record('quiz', $quiz); 1462 1463 $result = mod_quiz_external::process_attempt($attempt->id, array()); 1464 $result = external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result); 1465 $this->assertEquals(quiz_attempt::ABANDONED, $result['state']); 1466 1467 } 1468 1469 /** 1470 * Test validate_attempt_review 1471 */ 1472 public function test_validate_attempt_review() { 1473 global $DB; 1474 1475 // Create a new quiz with one attempt started. 1476 list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true); 1477 1478 $this->setUser($this->student); 1479 1480 // Invalid attempt, invalid id. 1481 try { 1482 $params = array('attemptid' => -1); 1483 testable_mod_quiz_external::validate_attempt_review($params); 1484 $this->fail('Exception expected due invalid id.'); 1485 } catch (dml_missing_record_exception $e) { 1486 $this->assertEquals('invalidrecord', $e->errorcode); 1487 } 1488 1489 // Invalid attempt, not closed. 1490 try { 1491 $params = array('attemptid' => $attempt->id); 1492 testable_mod_quiz_external::validate_attempt_review($params); 1493 $this->fail('Exception expected due not closed attempt.'); 1494 } catch (moodle_quiz_exception $e) { 1495 $this->assertEquals('attemptclosed', $e->errorcode); 1496 } 1497 1498 // Test ok case (finished attempt). 1499 list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true, true); 1500 1501 $params = array('attemptid' => $attempt->id); 1502 testable_mod_quiz_external::validate_attempt_review($params); 1503 1504 // Teacher should be able to view the review of one student's attempt. 1505 $this->setUser($this->teacher); 1506 testable_mod_quiz_external::validate_attempt_review($params); 1507 1508 // We should not see other students attempts. 1509 $anotherstudent = self::getDataGenerator()->create_user(); 1510 $this->getDataGenerator()->enrol_user($anotherstudent->id, $this->course->id, $this->studentrole->id, 'manual'); 1511 1512 $this->setUser($anotherstudent); 1513 try { 1514 $params = array('attemptid' => $attempt->id); 1515 testable_mod_quiz_external::validate_attempt_review($params); 1516 $this->fail('Exception expected due missing permissions.'); 1517 } catch (moodle_quiz_exception $e) { 1518 $this->assertEquals('noreviewattempt', $e->errorcode); 1519 } 1520 } 1521 1522 1523 /** 1524 * Test get_attempt_review 1525 */ 1526 public function test_get_attempt_review() { 1527 global $DB; 1528 1529 // Create a new quiz with two questions and one attempt finished. 1530 list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true, true); 1531 1532 // Add feedback to the quiz. 1533 $feedback = new stdClass(); 1534 $feedback->quizid = $quiz->id; 1535 $feedback->feedbacktext = 'Feedback text 1'; 1536 $feedback->feedbacktextformat = 1; 1537 $feedback->mingrade = 49; 1538 $feedback->maxgrade = 100; 1539 $feedback->id = $DB->insert_record('quiz_feedback', $feedback); 1540 1541 $feedback->feedbacktext = 'Feedback text 2'; 1542 $feedback->feedbacktextformat = 1; 1543 $feedback->mingrade = 30; 1544 $feedback->maxgrade = 48; 1545 $feedback->id = $DB->insert_record('quiz_feedback', $feedback); 1546 1547 $result = mod_quiz_external::get_attempt_review($attempt->id); 1548 $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_review_returns(), $result); 1549 1550 // Two questions, one completed and correct, the other gave up. 1551 $this->assertEquals(50, $result['grade']); 1552 $this->assertEquals(1, $result['attempt']['attempt']); 1553 $this->assertEquals('finished', $result['attempt']['state']); 1554 $this->assertEquals(1, $result['attempt']['sumgrades']); 1555 $this->assertCount(2, $result['questions']); 1556 $this->assertEquals('gradedright', $result['questions'][0]['state']); 1557 $this->assertEquals(1, $result['questions'][0]['slot']); 1558 $this->assertEquals('gaveup', $result['questions'][1]['state']); 1559 $this->assertEquals(2, $result['questions'][1]['slot']); 1560 1561 $this->assertCount(1, $result['additionaldata']); 1562 $this->assertEquals('feedback', $result['additionaldata'][0]['id']); 1563 $this->assertEquals('Feedback', $result['additionaldata'][0]['title']); 1564 $this->assertEquals('Feedback text 1', $result['additionaldata'][0]['content']); 1565 1566 // Only first page. 1567 $result = mod_quiz_external::get_attempt_review($attempt->id, 0); 1568 $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_review_returns(), $result); 1569 1570 $this->assertEquals(50, $result['grade']); 1571 $this->assertEquals(1, $result['attempt']['attempt']); 1572 $this->assertEquals('finished', $result['attempt']['state']); 1573 $this->assertEquals(1, $result['attempt']['sumgrades']); 1574 $this->assertCount(1, $result['questions']); 1575 $this->assertEquals('gradedright', $result['questions'][0]['state']); 1576 $this->assertEquals(1, $result['questions'][0]['slot']); 1577 1578 $this->assertCount(1, $result['additionaldata']); 1579 $this->assertEquals('feedback', $result['additionaldata'][0]['id']); 1580 $this->assertEquals('Feedback', $result['additionaldata'][0]['title']); 1581 $this->assertEquals('Feedback text 1', $result['additionaldata'][0]['content']); 1582 1583 } 1584 1585 /** 1586 * Test test_view_attempt 1587 */ 1588 public function test_view_attempt() { 1589 global $DB; 1590 1591 // Create a new quiz with two questions and one attempt started. 1592 list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true, false); 1593 1594 // Test user with full capabilities. 1595 $this->setUser($this->student); 1596 1597 // Trigger and capture the event. 1598 $sink = $this->redirectEvents(); 1599 1600 $result = mod_quiz_external::view_attempt($attempt->id, 0); 1601 $result = external_api::clean_returnvalue(mod_quiz_external::view_attempt_returns(), $result); 1602 $this->assertTrue($result['status']); 1603 1604 $events = $sink->get_events(); 1605 $this->assertCount(1, $events); 1606 $event = array_shift($events); 1607 1608 // Checking that the event contains the expected values. 1609 $this->assertInstanceOf('\mod_quiz\event\attempt_viewed', $event); 1610 $this->assertEquals($context, $event->get_context()); 1611 $this->assertEventContextNotUsed($event); 1612 $this->assertNotEmpty($event->get_name()); 1613 1614 // Now, force the quiz with QUIZ_NAVMETHOD_SEQ (sequencial) navigation method. 1615 $DB->set_field('quiz', 'navmethod', QUIZ_NAVMETHOD_SEQ, array('id' => $quiz->id)); 1616 // Quiz requiring preflightdata. 1617 $DB->set_field('quiz', 'password', 'abcdef', array('id' => $quiz->id)); 1618 $preflightdata = array(array("name" => "quizpassword", "value" => 'abcdef')); 1619 1620 // See next page. 1621 $result = mod_quiz_external::view_attempt($attempt->id, 1, $preflightdata); 1622 $result = external_api::clean_returnvalue(mod_quiz_external::view_attempt_returns(), $result); 1623 $this->assertTrue($result['status']); 1624 1625 $events = $sink->get_events(); 1626 $this->assertCount(2, $events); 1627 1628 // Try to go to previous page. 1629 try { 1630 mod_quiz_external::view_attempt($attempt->id, 0); 1631 $this->fail('Exception expected due to try to see a previous page.'); 1632 } catch (moodle_quiz_exception $e) { 1633 $this->assertEquals('Out of sequence access', $e->errorcode); 1634 } 1635 1636 } 1637 1638 /** 1639 * Test test_view_attempt_summary 1640 */ 1641 public function test_view_attempt_summary() { 1642 global $DB; 1643 1644 // Create a new quiz with two questions and one attempt started. 1645 list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true, false); 1646 1647 // Test user with full capabilities. 1648 $this->setUser($this->student); 1649 1650 // Trigger and capture the event. 1651 $sink = $this->redirectEvents(); 1652 1653 $result = mod_quiz_external::view_attempt_summary($attempt->id); 1654 $result = external_api::clean_returnvalue(mod_quiz_external::view_attempt_summary_returns(), $result); 1655 $this->assertTrue($result['status']); 1656 1657 $events = $sink->get_events(); 1658 $this->assertCount(1, $events); 1659 $event = array_shift($events); 1660 1661 // Checking that the event contains the expected values. 1662 $this->assertInstanceOf('\mod_quiz\event\attempt_summary_viewed', $event); 1663 $this->assertEquals($context, $event->get_context()); 1664 $moodlequiz = new \moodle_url('/mod/quiz/summary.php', array('attempt' => $attempt->id)); 1665 $this->assertEquals($moodlequiz, $event->get_url()); 1666 $this->assertEventContextNotUsed($event); 1667 $this->assertNotEmpty($event->get_name()); 1668 1669 // Quiz requiring preflightdata. 1670 $DB->set_field('quiz', 'password', 'abcdef', array('id' => $quiz->id)); 1671 $preflightdata = array(array("name" => "quizpassword", "value" => 'abcdef')); 1672 1673 $result = mod_quiz_external::view_attempt_summary($attempt->id, $preflightdata); 1674 $result = external_api::clean_returnvalue(mod_quiz_external::view_attempt_summary_returns(), $result); 1675 $this->assertTrue($result['status']); 1676 1677 } 1678 1679 /** 1680 * Test test_view_attempt_summary 1681 */ 1682 public function test_view_attempt_review() { 1683 global $DB; 1684 1685 // Create a new quiz with two questions and one attempt finished. 1686 list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true, true); 1687 1688 // Test user with full capabilities. 1689 $this->setUser($this->student); 1690 1691 // Trigger and capture the event. 1692 $sink = $this->redirectEvents(); 1693 1694 $result = mod_quiz_external::view_attempt_review($attempt->id, 0); 1695 $result = external_api::clean_returnvalue(mod_quiz_external::view_attempt_review_returns(), $result); 1696 $this->assertTrue($result['status']); 1697 1698 $events = $sink->get_events(); 1699 $this->assertCount(1, $events); 1700 $event = array_shift($events); 1701 1702 // Checking that the event contains the expected values. 1703 $this->assertInstanceOf('\mod_quiz\event\attempt_reviewed', $event); 1704 $this->assertEquals($context, $event->get_context()); 1705 $moodlequiz = new \moodle_url('/mod/quiz/review.php', array('attempt' => $attempt->id)); 1706 $this->assertEquals($moodlequiz, $event->get_url()); 1707 $this->assertEventContextNotUsed($event); 1708 $this->assertNotEmpty($event->get_name()); 1709 1710 } 1711 1712 /** 1713 * Test get_quiz_feedback_for_grade 1714 */ 1715 public function test_get_quiz_feedback_for_grade() { 1716 global $DB; 1717 1718 // Add feedback to the quiz. 1719 $feedback = new stdClass(); 1720 $feedback->quizid = $this->quiz->id; 1721 $feedback->feedbacktext = 'Feedback text 1'; 1722 $feedback->feedbacktextformat = 1; 1723 $feedback->mingrade = 49; 1724 $feedback->maxgrade = 100; 1725 $feedback->id = $DB->insert_record('quiz_feedback', $feedback); 1726 // Add a fake inline image to the feedback text. 1727 $filename = 'shouldbeanimage.jpg'; 1728 $filerecordinline = array( 1729 'contextid' => $this->context->id, 1730 'component' => 'mod_quiz', 1731 'filearea' => 'feedback', 1732 'itemid' => $feedback->id, 1733 'filepath' => '/', 1734 'filename' => $filename, 1735 ); 1736 $fs = get_file_storage(); 1737 $fs->create_file_from_string($filerecordinline, 'image contents (not really)'); 1738 1739 $feedback->feedbacktext = 'Feedback text 2'; 1740 $feedback->feedbacktextformat = 1; 1741 $feedback->mingrade = 30; 1742 $feedback->maxgrade = 49; 1743 $feedback->id = $DB->insert_record('quiz_feedback', $feedback); 1744 1745 $result = mod_quiz_external::get_quiz_feedback_for_grade($this->quiz->id, 50); 1746 $result = external_api::clean_returnvalue(mod_quiz_external::get_quiz_feedback_for_grade_returns(), $result); 1747 $this->assertEquals('Feedback text 1', $result['feedbacktext']); 1748 $this->assertEquals($filename, $result['feedbackinlinefiles'][0]['filename']); 1749 $this->assertEquals(FORMAT_HTML, $result['feedbacktextformat']); 1750 1751 $result = mod_quiz_external::get_quiz_feedback_for_grade($this->quiz->id, 30); 1752 $result = external_api::clean_returnvalue(mod_quiz_external::get_quiz_feedback_for_grade_returns(), $result); 1753 $this->assertEquals('Feedback text 2', $result['feedbacktext']); 1754 $this->assertEquals(FORMAT_HTML, $result['feedbacktextformat']); 1755 1756 $result = mod_quiz_external::get_quiz_feedback_for_grade($this->quiz->id, 10); 1757 $result = external_api::clean_returnvalue(mod_quiz_external::get_quiz_feedback_for_grade_returns(), $result); 1758 $this->assertEquals('', $result['feedbacktext']); 1759 $this->assertEquals(FORMAT_MOODLE, $result['feedbacktextformat']); 1760 } 1761 1762 /** 1763 * Test get_quiz_access_information 1764 */ 1765 public function test_get_quiz_access_information() { 1766 global $DB; 1767 1768 // Create a new quiz. 1769 $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); 1770 $data = array('course' => $this->course->id); 1771 $quiz = $quizgenerator->create_instance($data); 1772 1773 $this->setUser($this->student); 1774 1775 // Default restrictions (none). 1776 $result = mod_quiz_external::get_quiz_access_information($quiz->id); 1777 $result = external_api::clean_returnvalue(mod_quiz_external::get_quiz_access_information_returns(), $result); 1778 1779 $expected = array( 1780 'canattempt' => true, 1781 'canmanage' => false, 1782 'canpreview' => false, 1783 'canreviewmyattempts' => true, 1784 'canviewreports' => false, 1785 'accessrules' => [], 1786 // This rule is always used, even if the quiz has no open or close date. 1787 'activerulenames' => ['quizaccess_openclosedate'], 1788 'preventaccessreasons' => [], 1789 'warnings' => [] 1790 ); 1791 1792 $this->assertEquals($expected, $result); 1793 1794 // Now teacher, different privileges. 1795 $this->setUser($this->teacher); 1796 $result = mod_quiz_external::get_quiz_access_information($quiz->id); 1797 $result = external_api::clean_returnvalue(mod_quiz_external::get_quiz_access_information_returns(), $result); 1798 1799 $expected['canmanage'] = true; 1800 $expected['canpreview'] = true; 1801 $expected['canviewreports'] = true; 1802 $expected['canattempt'] = false; 1803 $expected['canreviewmyattempts'] = false; 1804 1805 $this->assertEquals($expected, $result); 1806 1807 $this->setUser($this->student); 1808 // Now add some restrictions. 1809 $quiz->timeopen = time() + DAYSECS; 1810 $quiz->timeclose = time() + WEEKSECS; 1811 $quiz->password = '123456'; 1812 $DB->update_record('quiz', $quiz); 1813 1814 $result = mod_quiz_external::get_quiz_access_information($quiz->id); 1815 $result = external_api::clean_returnvalue(mod_quiz_external::get_quiz_access_information_returns(), $result); 1816 1817 // Access limited by time and password. 1818 $this->assertCount(3, $result['accessrules']); 1819 // Two rule names, password and open/close date. 1820 $this->assertCount(2, $result['activerulenames']); 1821 $this->assertCount(1, $result['preventaccessreasons']); 1822 1823 } 1824 1825 /** 1826 * Test get_attempt_access_information 1827 */ 1828 public function test_get_attempt_access_information() { 1829 global $DB; 1830 1831 $this->setAdminUser(); 1832 1833 // Create a new quiz with attempts. 1834 $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); 1835 $data = array('course' => $this->course->id, 1836 'sumgrades' => 2); 1837 $quiz = $quizgenerator->create_instance($data); 1838 1839 // Create some questions. 1840 $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); 1841 1842 $cat = $questiongenerator->create_question_category(); 1843 $question = $questiongenerator->create_question('numerical', null, array('category' => $cat->id)); 1844 quiz_add_quiz_question($question->id, $quiz); 1845 1846 $question = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id)); 1847 quiz_add_quiz_question($question->id, $quiz); 1848 1849 // Add new question types in the category (for the random one). 1850 $question = $questiongenerator->create_question('truefalse', null, array('category' => $cat->id)); 1851 $question = $questiongenerator->create_question('essay', null, array('category' => $cat->id)); 1852 1853 quiz_add_random_questions($quiz, 0, $cat->id, 1, false); 1854 1855 $quizobj = quiz::create($quiz->id, $this->student->id); 1856 1857 // Set grade to pass. 1858 $item = grade_item::fetch(array('courseid' => $this->course->id, 'itemtype' => 'mod', 1859 'itemmodule' => 'quiz', 'iteminstance' => $quiz->id, 'outcomeid' => null)); 1860 $item->gradepass = 80; 1861 $item->update(); 1862 1863 $this->setUser($this->student); 1864 1865 // Default restrictions (none). 1866 $result = mod_quiz_external::get_attempt_access_information($quiz->id); 1867 $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_access_information_returns(), $result); 1868 1869 $expected = array( 1870 'isfinished' => false, 1871 'preventnewattemptreasons' => [], 1872 'warnings' => [] 1873 ); 1874 1875 $this->assertEquals($expected, $result); 1876 1877 // Limited attempts. 1878 $quiz->attempts = 1; 1879 $DB->update_record('quiz', $quiz); 1880 1881 // Now, do one attempt. 1882 $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); 1883 $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); 1884 1885 $timenow = time(); 1886 $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $this->student->id); 1887 quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow); 1888 quiz_attempt_save_started($quizobj, $quba, $attempt); 1889 1890 // Process some responses from the student. 1891 $attemptobj = quiz_attempt::create($attempt->id); 1892 $tosubmit = array(1 => array('answer' => '3.14')); 1893 $attemptobj->process_submitted_actions($timenow, false, $tosubmit); 1894 1895 // Finish the attempt. 1896 $attemptobj = quiz_attempt::create($attempt->id); 1897 $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question()); 1898 $attemptobj->process_finish($timenow, false); 1899 1900 // Can we start a new attempt? We shall not! 1901 $result = mod_quiz_external::get_attempt_access_information($quiz->id, $attempt->id); 1902 $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_access_information_returns(), $result); 1903 1904 // Now new attemps allowed. 1905 $this->assertCount(1, $result['preventnewattemptreasons']); 1906 $this->assertFalse($result['ispreflightcheckrequired']); 1907 $this->assertEquals(get_string('nomoreattempts', 'quiz'), $result['preventnewattemptreasons'][0]); 1908 1909 } 1910 1911 /** 1912 * Test get_quiz_required_qtypes 1913 */ 1914 public function test_get_quiz_required_qtypes() { 1915 $this->setAdminUser(); 1916 1917 // Create a new quiz. 1918 $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); 1919 $data = array('course' => $this->course->id); 1920 $quiz = $quizgenerator->create_instance($data); 1921 1922 // Create some questions. 1923 $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); 1924 1925 $cat = $questiongenerator->create_question_category(); 1926 $question = $questiongenerator->create_question('numerical', null, array('category' => $cat->id)); 1927 quiz_add_quiz_question($question->id, $quiz); 1928 1929 $question = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id)); 1930 quiz_add_quiz_question($question->id, $quiz); 1931 1932 // Add new question types in the category (for the random one). 1933 $question = $questiongenerator->create_question('truefalse', null, array('category' => $cat->id)); 1934 $question = $questiongenerator->create_question('essay', null, array('category' => $cat->id)); 1935 1936 quiz_add_random_questions($quiz, 0, $cat->id, 1, false); 1937 1938 $this->setUser($this->student); 1939 1940 $result = mod_quiz_external::get_quiz_required_qtypes($quiz->id); 1941 $result = external_api::clean_returnvalue(mod_quiz_external::get_quiz_required_qtypes_returns(), $result); 1942 1943 $expected = array( 1944 'questiontypes' => ['essay', 'numerical', 'random', 'shortanswer', 'truefalse'], 1945 'warnings' => [] 1946 ); 1947 1948 $this->assertEquals($expected, $result); 1949 1950 } 1951 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body