[ Index ] |
PHP Cross Reference of Unnamed Project |
[Summary view] [Print] [Text view]
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 $this->getDataGenerator()->enrol_user($this->student->id, $this->course->id, $this->studentrole->id, 'manual'); 99 $this->getDataGenerator()->enrol_user($this->teacher->id, $this->course->id, $this->teacherrole->id, 'manual'); 100 } 101 102 /** 103 * Create a quiz with questions including a started or finished attempt optionally 104 * 105 * @param boolean $startattempt whether to start a new attempt 106 * @param boolean $finishattempt whether to finish the new attempt 107 * @return array array containing the quiz, context and the attempt 108 */ 109 private function create_quiz_with_questions($startattempt = false, $finishattempt = false) { 110 111 // Create a new quiz with attempts. 112 $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); 113 $data = array('course' => $this->course->id, 114 'sumgrades' => 2); 115 $quiz = $quizgenerator->create_instance($data); 116 $context = context_module::instance($quiz->cmid); 117 118 // Create a couple of questions. 119 $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); 120 121 $cat = $questiongenerator->create_question_category(); 122 $question = $questiongenerator->create_question('numerical', null, array('category' => $cat->id)); 123 quiz_add_quiz_question($question->id, $quiz); 124 $question = $questiongenerator->create_question('numerical', null, array('category' => $cat->id)); 125 quiz_add_quiz_question($question->id, $quiz); 126 127 $quizobj = quiz::create($quiz->id, $this->student->id); 128 129 // Set grade to pass. 130 $item = grade_item::fetch(array('courseid' => $this->course->id, 'itemtype' => 'mod', 131 'itemmodule' => 'quiz', 'iteminstance' => $quiz->id, 'outcomeid' => null)); 132 $item->gradepass = 80; 133 $item->update(); 134 135 if ($startattempt or $finishattempt) { 136 // Now, do one attempt. 137 $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); 138 $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); 139 140 $timenow = time(); 141 $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $this->student->id); 142 quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow); 143 quiz_attempt_save_started($quizobj, $quba, $attempt); 144 $attemptobj = quiz_attempt::create($attempt->id); 145 146 if ($finishattempt) { 147 // Process some responses from the student. 148 $tosubmit = array(1 => array('answer' => '3.14')); 149 $attemptobj->process_submitted_actions(time(), false, $tosubmit); 150 151 // Finish the attempt. 152 $attemptobj->process_finish(time(), false); 153 } 154 return array($quiz, $context, $quizobj, $attempt, $attemptobj, $quba); 155 } else { 156 return array($quiz, $context, $quizobj); 157 } 158 159 } 160 161 /* 162 * Test get quizzes by courses 163 */ 164 public function test_mod_quiz_get_quizzes_by_courses() { 165 global $DB; 166 167 // Create additional course. 168 $course2 = self::getDataGenerator()->create_course(); 169 170 // Second quiz. 171 $record = new stdClass(); 172 $record->course = $course2->id; 173 $quiz2 = self::getDataGenerator()->create_module('quiz', $record); 174 175 // Execute real Moodle enrolment as we'll call unenrol() method on the instance later. 176 $enrol = enrol_get_plugin('manual'); 177 $enrolinstances = enrol_get_instances($course2->id, true); 178 foreach ($enrolinstances as $courseenrolinstance) { 179 if ($courseenrolinstance->enrol == "manual") { 180 $instance2 = $courseenrolinstance; 181 break; 182 } 183 } 184 $enrol->enrol_user($instance2, $this->student->id, $this->studentrole->id); 185 186 self::setUser($this->student); 187 188 $returndescription = mod_quiz_external::get_quizzes_by_courses_returns(); 189 190 // Create what we expect to be returned when querying the two courses. 191 // First for the student user. 192 $allusersfields = array('id', 'coursemodule', 'course', 'name', 'intro', 'introformat', 'introfiles', 'timeopen', 193 'timeclose', 'grademethod', 'section', 'visible', 'groupmode', 'groupingid'); 194 $userswithaccessfields = array('timelimit', 'attempts', 'attemptonlast', 'grademethod', 'decimalpoints', 195 'questiondecimalpoints', 'reviewattempt', 'reviewcorrectness', 'reviewmarks', 196 'reviewspecificfeedback', 'reviewgeneralfeedback', 'reviewrightanswer', 197 'reviewoverallfeedback', 'questionsperpage', 'navmethod', 'sumgrades', 'grade', 198 'browsersecurity', 'delay1', 'delay2', 'showuserpicture', 'showblocks', 199 'completionattemptsexhausted', 'completionpass', 'autosaveperiod', 'hasquestions', 200 'hasfeedback', 'overduehandling', 'graceperiod', 'preferredbehaviour', 'canredoquestions'); 201 $managerfields = array('shuffleanswers', 'timecreated', 'timemodified', 'password', 'subnet'); 202 203 // Add expected coursemodule and other data. 204 $quiz1 = $this->quiz; 205 $quiz1->coursemodule = $quiz1->cmid; 206 $quiz1->introformat = 1; 207 $quiz1->section = 0; 208 $quiz1->visible = true; 209 $quiz1->groupmode = 0; 210 $quiz1->groupingid = 0; 211 $quiz1->hasquestions = 0; 212 $quiz1->hasfeedback = 0; 213 $quiz1->autosaveperiod = get_config('quiz', 'autosaveperiod'); 214 $quiz1->introfiles = []; 215 216 $quiz2->coursemodule = $quiz2->cmid; 217 $quiz2->introformat = 1; 218 $quiz2->section = 0; 219 $quiz2->visible = true; 220 $quiz2->groupmode = 0; 221 $quiz2->groupingid = 0; 222 $quiz2->hasquestions = 0; 223 $quiz2->hasfeedback = 0; 224 $quiz2->autosaveperiod = get_config('quiz', 'autosaveperiod'); 225 $quiz2->introfiles = []; 226 227 foreach (array_merge($allusersfields, $userswithaccessfields) as $field) { 228 $expected1[$field] = $quiz1->{$field}; 229 $expected2[$field] = $quiz2->{$field}; 230 } 231 232 $expectedquizzes = array($expected2, $expected1); 233 234 // Call the external function passing course ids. 235 $result = mod_quiz_external::get_quizzes_by_courses(array($course2->id, $this->course->id)); 236 $result = external_api::clean_returnvalue($returndescription, $result); 237 238 $this->assertEquals($expectedquizzes, $result['quizzes']); 239 $this->assertCount(0, $result['warnings']); 240 241 // Call the external function without passing course id. 242 $result = mod_quiz_external::get_quizzes_by_courses(); 243 $result = external_api::clean_returnvalue($returndescription, $result); 244 $this->assertEquals($expectedquizzes, $result['quizzes']); 245 $this->assertCount(0, $result['warnings']); 246 247 // Unenrol user from second course and alter expected quizzes. 248 $enrol->unenrol_user($instance2, $this->student->id); 249 array_shift($expectedquizzes); 250 251 // Call the external function without passing course id. 252 $result = mod_quiz_external::get_quizzes_by_courses(); 253 $result = external_api::clean_returnvalue($returndescription, $result); 254 $this->assertEquals($expectedquizzes, $result['quizzes']); 255 256 // Call for the second course we unenrolled the user from, expected warning. 257 $result = mod_quiz_external::get_quizzes_by_courses(array($course2->id)); 258 $this->assertCount(1, $result['warnings']); 259 $this->assertEquals('1', $result['warnings'][0]['warningcode']); 260 $this->assertEquals($course2->id, $result['warnings'][0]['itemid']); 261 262 // Now, try as a teacher for getting all the additional fields. 263 self::setUser($this->teacher); 264 265 foreach ($managerfields as $field) { 266 $expectedquizzes[0][$field] = $quiz1->{$field}; 267 } 268 269 $result = mod_quiz_external::get_quizzes_by_courses(); 270 $result = external_api::clean_returnvalue($returndescription, $result); 271 $this->assertEquals($expectedquizzes, $result['quizzes']); 272 273 // Admin also should get all the information. 274 self::setAdminUser(); 275 276 $result = mod_quiz_external::get_quizzes_by_courses(array($this->course->id)); 277 $result = external_api::clean_returnvalue($returndescription, $result); 278 $this->assertEquals($expectedquizzes, $result['quizzes']); 279 280 // Now, prevent access. 281 $enrol->enrol_user($instance2, $this->student->id); 282 283 self::setUser($this->student); 284 285 $quiz2->timeclose = time() - DAYSECS; 286 $DB->update_record('quiz', $quiz2); 287 288 $result = mod_quiz_external::get_quizzes_by_courses(); 289 $result = external_api::clean_returnvalue($returndescription, $result); 290 $this->assertCount(2, $result['quizzes']); 291 // We only see a limited set of fields. 292 $this->assertCount(4, $result['quizzes'][0]); 293 $this->assertEquals($quiz2->id, $result['quizzes'][0]['id']); 294 $this->assertEquals($quiz2->coursemodule, $result['quizzes'][0]['coursemodule']); 295 $this->assertEquals($quiz2->course, $result['quizzes'][0]['course']); 296 $this->assertEquals($quiz2->name, $result['quizzes'][0]['name']); 297 $this->assertEquals($quiz2->course, $result['quizzes'][0]['course']); 298 299 $this->assertFalse(isset($result['quizzes'][0]['timelimit'])); 300 301 } 302 303 /** 304 * Test test_view_quiz 305 */ 306 public function test_view_quiz() { 307 global $DB; 308 309 // Test invalid instance id. 310 try { 311 mod_quiz_external::view_quiz(0); 312 $this->fail('Exception expected due to invalid mod_quiz instance id.'); 313 } catch (moodle_exception $e) { 314 $this->assertEquals('invalidrecord', $e->errorcode); 315 } 316 317 // Test not-enrolled user. 318 $usernotenrolled = self::getDataGenerator()->create_user(); 319 $this->setUser($usernotenrolled); 320 try { 321 mod_quiz_external::view_quiz($this->quiz->id); 322 $this->fail('Exception expected due to not enrolled user.'); 323 } catch (moodle_exception $e) { 324 $this->assertEquals('requireloginerror', $e->errorcode); 325 } 326 327 // Test user with full capabilities. 328 $this->setUser($this->student); 329 330 // Trigger and capture the event. 331 $sink = $this->redirectEvents(); 332 333 $result = mod_quiz_external::view_quiz($this->quiz->id); 334 $result = external_api::clean_returnvalue(mod_quiz_external::view_quiz_returns(), $result); 335 $this->assertTrue($result['status']); 336 337 $events = $sink->get_events(); 338 $this->assertCount(1, $events); 339 $event = array_shift($events); 340 341 // Checking that the event contains the expected values. 342 $this->assertInstanceOf('\mod_quiz\event\course_module_viewed', $event); 343 $this->assertEquals($this->context, $event->get_context()); 344 $moodlequiz = new \moodle_url('/mod/quiz/view.php', array('id' => $this->cm->id)); 345 $this->assertEquals($moodlequiz, $event->get_url()); 346 $this->assertEventContextNotUsed($event); 347 $this->assertNotEmpty($event->get_name()); 348 349 // Test user with no capabilities. 350 // We need a explicit prohibit since this capability is only defined in authenticated user and guest roles. 351 assign_capability('mod/quiz:view', CAP_PROHIBIT, $this->studentrole->id, $this->context->id); 352 // Empty all the caches that may be affected by this change. 353 accesslib_clear_all_caches_for_unit_testing(); 354 course_modinfo::clear_instance_cache(); 355 356 try { 357 mod_quiz_external::view_quiz($this->quiz->id); 358 $this->fail('Exception expected due to missing capability.'); 359 } catch (moodle_exception $e) { 360 $this->assertEquals('requireloginerror', $e->errorcode); 361 } 362 363 } 364 365 /** 366 * Test get_user_attempts 367 */ 368 public function test_get_user_attempts() { 369 370 // Create a quiz with one attempt finished. 371 list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true, true); 372 373 $this->setUser($this->student); 374 $result = mod_quiz_external::get_user_attempts($quiz->id); 375 $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result); 376 377 $this->assertCount(1, $result['attempts']); 378 $this->assertEquals($attempt->id, $result['attempts'][0]['id']); 379 $this->assertEquals($quiz->id, $result['attempts'][0]['quiz']); 380 $this->assertEquals($this->student->id, $result['attempts'][0]['userid']); 381 $this->assertEquals(1, $result['attempts'][0]['attempt']); 382 383 // Test filters. Only finished. 384 $result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'finished', false); 385 $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result); 386 387 $this->assertCount(1, $result['attempts']); 388 $this->assertEquals($attempt->id, $result['attempts'][0]['id']); 389 390 // Test filters. All attempts. 391 $result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'all', false); 392 $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result); 393 394 $this->assertCount(1, $result['attempts']); 395 $this->assertEquals($attempt->id, $result['attempts'][0]['id']); 396 397 // Test filters. Unfinished. 398 $result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'unfinished', false); 399 $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result); 400 401 $this->assertCount(0, $result['attempts']); 402 403 // Start a new attempt, but not finish it. 404 $timenow = time(); 405 $attempt = quiz_create_attempt($quizobj, 2, false, $timenow, false, $this->student->id); 406 $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); 407 $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); 408 409 quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow); 410 quiz_attempt_save_started($quizobj, $quba, $attempt); 411 412 // Test filters. All attempts. 413 $result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'all', false); 414 $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result); 415 416 $this->assertCount(2, $result['attempts']); 417 418 // Test filters. Unfinished. 419 $result = mod_quiz_external::get_user_attempts($quiz->id, 0, 'unfinished', false); 420 $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result); 421 422 $this->assertCount(1, $result['attempts']); 423 424 // Test manager can see user attempts. 425 $this->setUser($this->teacher); 426 $result = mod_quiz_external::get_user_attempts($quiz->id, $this->student->id); 427 $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result); 428 429 $this->assertCount(1, $result['attempts']); 430 $this->assertEquals($this->student->id, $result['attempts'][0]['userid']); 431 432 $result = mod_quiz_external::get_user_attempts($quiz->id, $this->student->id, 'all'); 433 $result = external_api::clean_returnvalue(mod_quiz_external::get_user_attempts_returns(), $result); 434 435 $this->assertCount(2, $result['attempts']); 436 $this->assertEquals($this->student->id, $result['attempts'][0]['userid']); 437 438 // Invalid parameters. 439 try { 440 mod_quiz_external::get_user_attempts($quiz->id, $this->student->id, 'INVALID_PARAMETER'); 441 $this->fail('Exception expected due to missing capability.'); 442 } catch (invalid_parameter_exception $e) { 443 $this->assertEquals('invalidparameter', $e->errorcode); 444 } 445 } 446 447 /** 448 * Test get_user_best_grade 449 */ 450 public function test_get_user_best_grade() { 451 global $DB; 452 453 $this->setUser($this->student); 454 455 $result = mod_quiz_external::get_user_best_grade($this->quiz->id); 456 $result = external_api::clean_returnvalue(mod_quiz_external::get_user_best_grade_returns(), $result); 457 458 // No grades yet. 459 $this->assertFalse($result['hasgrade']); 460 $this->assertTrue(!isset($result['grade'])); 461 462 $grade = new stdClass(); 463 $grade->quiz = $this->quiz->id; 464 $grade->userid = $this->student->id; 465 $grade->grade = 8.9; 466 $grade->timemodified = time(); 467 $grade->id = $DB->insert_record('quiz_grades', $grade); 468 469 $result = mod_quiz_external::get_user_best_grade($this->quiz->id); 470 $result = external_api::clean_returnvalue(mod_quiz_external::get_user_best_grade_returns(), $result); 471 472 // Now I have grades. 473 $this->assertTrue($result['hasgrade']); 474 $this->assertEquals(8.9, $result['grade']); 475 476 // We should not see other users grades. 477 $anotherstudent = self::getDataGenerator()->create_user(); 478 $this->getDataGenerator()->enrol_user($anotherstudent->id, $this->course->id, $this->studentrole->id, 'manual'); 479 480 try { 481 mod_quiz_external::get_user_best_grade($this->quiz->id, $anotherstudent->id); 482 $this->fail('Exception expected due to missing capability.'); 483 } catch (required_capability_exception $e) { 484 $this->assertEquals('nopermissions', $e->errorcode); 485 } 486 487 // Teacher must be able to see student grades. 488 $this->setUser($this->teacher); 489 490 $result = mod_quiz_external::get_user_best_grade($this->quiz->id, $this->student->id); 491 $result = external_api::clean_returnvalue(mod_quiz_external::get_user_best_grade_returns(), $result); 492 493 $this->assertTrue($result['hasgrade']); 494 $this->assertEquals(8.9, $result['grade']); 495 496 // Invalid user. 497 try { 498 mod_quiz_external::get_user_best_grade($this->quiz->id, -1); 499 $this->fail('Exception expected due to missing capability.'); 500 } catch (dml_missing_record_exception $e) { 501 $this->assertEquals('invaliduser', $e->errorcode); 502 } 503 504 // Remove the created data. 505 $DB->delete_records('quiz_grades', array('id' => $grade->id)); 506 507 } 508 /** 509 * Test get_combined_review_options. 510 * This is a basic test, this is already tested in mod_quiz_display_options_testcase. 511 */ 512 public function test_get_combined_review_options() { 513 global $DB; 514 515 // Create a new quiz with attempts. 516 $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); 517 $data = array('course' => $this->course->id, 518 'sumgrades' => 1); 519 $quiz = $quizgenerator->create_instance($data); 520 521 // Create a couple of questions. 522 $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); 523 524 $cat = $questiongenerator->create_question_category(); 525 $question = $questiongenerator->create_question('numerical', null, array('category' => $cat->id)); 526 quiz_add_quiz_question($question->id, $quiz); 527 528 $quizobj = quiz::create($quiz->id, $this->student->id); 529 530 // Set grade to pass. 531 $item = grade_item::fetch(array('courseid' => $this->course->id, 'itemtype' => 'mod', 532 'itemmodule' => 'quiz', 'iteminstance' => $quiz->id, 'outcomeid' => null)); 533 $item->gradepass = 80; 534 $item->update(); 535 536 // Start the passing attempt. 537 $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); 538 $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); 539 540 $timenow = time(); 541 $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $this->student->id); 542 quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow); 543 quiz_attempt_save_started($quizobj, $quba, $attempt); 544 545 $this->setUser($this->student); 546 547 $result = mod_quiz_external::get_combined_review_options($quiz->id); 548 $result = external_api::clean_returnvalue(mod_quiz_external::get_combined_review_options_returns(), $result); 549 550 // Expected values. 551 $expected = array( 552 "someoptions" => array( 553 array("name" => "feedback", "value" => 1), 554 array("name" => "generalfeedback", "value" => 1), 555 array("name" => "rightanswer", "value" => 1), 556 array("name" => "overallfeedback", "value" => 0), 557 array("name" => "marks", "value" => 2), 558 ), 559 "alloptions" => array( 560 array("name" => "feedback", "value" => 1), 561 array("name" => "generalfeedback", "value" => 1), 562 array("name" => "rightanswer", "value" => 1), 563 array("name" => "overallfeedback", "value" => 0), 564 array("name" => "marks", "value" => 2), 565 ), 566 "warnings" => [], 567 ); 568 569 $this->assertEquals($expected, $result); 570 571 // Now, finish the attempt. 572 $attemptobj = quiz_attempt::create($attempt->id); 573 $attemptobj->process_finish($timenow, false); 574 575 $expected = array( 576 "someoptions" => array( 577 array("name" => "feedback", "value" => 1), 578 array("name" => "generalfeedback", "value" => 1), 579 array("name" => "rightanswer", "value" => 1), 580 array("name" => "overallfeedback", "value" => 1), 581 array("name" => "marks", "value" => 2), 582 ), 583 "alloptions" => array( 584 array("name" => "feedback", "value" => 1), 585 array("name" => "generalfeedback", "value" => 1), 586 array("name" => "rightanswer", "value" => 1), 587 array("name" => "overallfeedback", "value" => 1), 588 array("name" => "marks", "value" => 2), 589 ), 590 "warnings" => [], 591 ); 592 593 // We should see now the overall feedback. 594 $result = mod_quiz_external::get_combined_review_options($quiz->id); 595 $result = external_api::clean_returnvalue(mod_quiz_external::get_combined_review_options_returns(), $result); 596 $this->assertEquals($expected, $result); 597 598 // Start a new attempt, but not finish it. 599 $timenow = time(); 600 $attempt = quiz_create_attempt($quizobj, 2, false, $timenow, false, $this->student->id); 601 $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); 602 $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); 603 quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow); 604 quiz_attempt_save_started($quizobj, $quba, $attempt); 605 606 $expected = array( 607 "someoptions" => array( 608 array("name" => "feedback", "value" => 1), 609 array("name" => "generalfeedback", "value" => 1), 610 array("name" => "rightanswer", "value" => 1), 611 array("name" => "overallfeedback", "value" => 1), 612 array("name" => "marks", "value" => 2), 613 ), 614 "alloptions" => array( 615 array("name" => "feedback", "value" => 1), 616 array("name" => "generalfeedback", "value" => 1), 617 array("name" => "rightanswer", "value" => 1), 618 array("name" => "overallfeedback", "value" => 0), 619 array("name" => "marks", "value" => 2), 620 ), 621 "warnings" => [], 622 ); 623 624 $result = mod_quiz_external::get_combined_review_options($quiz->id); 625 $result = external_api::clean_returnvalue(mod_quiz_external::get_combined_review_options_returns(), $result); 626 $this->assertEquals($expected, $result); 627 628 // Teacher, for see student options. 629 $this->setUser($this->teacher); 630 631 $result = mod_quiz_external::get_combined_review_options($quiz->id, $this->student->id); 632 $result = external_api::clean_returnvalue(mod_quiz_external::get_combined_review_options_returns(), $result); 633 634 $this->assertEquals($expected, $result); 635 636 // Invalid user. 637 try { 638 mod_quiz_external::get_combined_review_options($quiz->id, -1); 639 $this->fail('Exception expected due to missing capability.'); 640 } catch (dml_missing_record_exception $e) { 641 $this->assertEquals('invaliduser', $e->errorcode); 642 } 643 } 644 645 /** 646 * Test start_attempt 647 */ 648 public function test_start_attempt() { 649 global $DB; 650 651 // Create a new quiz with questions. 652 list($quiz, $context, $quizobj) = $this->create_quiz_with_questions(); 653 654 $this->setUser($this->student); 655 656 // Try to open attempt in closed quiz. 657 $quiz->timeopen = time() - WEEKSECS; 658 $quiz->timeclose = time() - DAYSECS; 659 $DB->update_record('quiz', $quiz); 660 $result = mod_quiz_external::start_attempt($quiz->id); 661 $result = external_api::clean_returnvalue(mod_quiz_external::start_attempt_returns(), $result); 662 663 $this->assertEquals([], $result['attempt']); 664 $this->assertCount(1, $result['warnings']); 665 666 // Now with a password. 667 $quiz->timeopen = 0; 668 $quiz->timeclose = 0; 669 $quiz->password = 'abc'; 670 $DB->update_record('quiz', $quiz); 671 672 try { 673 mod_quiz_external::start_attempt($quiz->id, array(array("name" => "quizpassword", "value" => 'bad'))); 674 $this->fail('Exception expected due to invalid passwod.'); 675 } catch (moodle_exception $e) { 676 $this->assertEquals(get_string('passworderror', 'quizaccess_password'), $e->errorcode); 677 } 678 679 // Now, try everything correct. 680 $result = mod_quiz_external::start_attempt($quiz->id, array(array("name" => "quizpassword", "value" => 'abc'))); 681 $result = external_api::clean_returnvalue(mod_quiz_external::start_attempt_returns(), $result); 682 683 $this->assertEquals(1, $result['attempt']['attempt']); 684 $this->assertEquals($this->student->id, $result['attempt']['userid']); 685 $this->assertEquals($quiz->id, $result['attempt']['quiz']); 686 $this->assertCount(0, $result['warnings']); 687 $attemptid = $result['attempt']['id']; 688 689 // We are good, try to start a new attempt now. 690 691 try { 692 mod_quiz_external::start_attempt($quiz->id, array(array("name" => "quizpassword", "value" => 'abc'))); 693 $this->fail('Exception expected due to attempt not finished.'); 694 } catch (moodle_quiz_exception $e) { 695 $this->assertEquals('attemptstillinprogress', $e->errorcode); 696 } 697 698 // Finish the started attempt. 699 700 // Process some responses from the student. 701 $timenow = time(); 702 $attemptobj = quiz_attempt::create($attemptid); 703 $tosubmit = array(1 => array('answer' => '3.14')); 704 $attemptobj->process_submitted_actions($timenow, false, $tosubmit); 705 706 // Finish the attempt. 707 $attemptobj = quiz_attempt::create($attemptid); 708 $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question()); 709 $attemptobj->process_finish($timenow, false); 710 711 // We should be able to start a new attempt. 712 $result = mod_quiz_external::start_attempt($quiz->id, array(array("name" => "quizpassword", "value" => 'abc'))); 713 $result = external_api::clean_returnvalue(mod_quiz_external::start_attempt_returns(), $result); 714 715 $this->assertEquals(2, $result['attempt']['attempt']); 716 $this->assertEquals($this->student->id, $result['attempt']['userid']); 717 $this->assertEquals($quiz->id, $result['attempt']['quiz']); 718 $this->assertCount(0, $result['warnings']); 719 720 // Test user with no capabilities. 721 // We need a explicit prohibit since this capability is only defined in authenticated user and guest roles. 722 assign_capability('mod/quiz:attempt', CAP_PROHIBIT, $this->studentrole->id, $context->id); 723 // Empty all the caches that may be affected by this change. 724 accesslib_clear_all_caches_for_unit_testing(); 725 course_modinfo::clear_instance_cache(); 726 727 try { 728 mod_quiz_external::start_attempt($quiz->id); 729 $this->fail('Exception expected due to missing capability.'); 730 } catch (required_capability_exception $e) { 731 $this->assertEquals('nopermissions', $e->errorcode); 732 } 733 734 } 735 736 /** 737 * Test validate_attempt 738 */ 739 public function test_validate_attempt() { 740 global $DB; 741 742 // Create a new quiz with one attempt started. 743 list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true); 744 745 $this->setUser($this->student); 746 747 // Invalid attempt. 748 try { 749 $params = array('attemptid' => -1, 'page' => 0); 750 testable_mod_quiz_external::validate_attempt($params); 751 $this->fail('Exception expected due to invalid attempt id.'); 752 } catch (dml_missing_record_exception $e) { 753 $this->assertEquals('invalidrecord', $e->errorcode); 754 } 755 756 // Test OK case. 757 $params = array('attemptid' => $attempt->id, 'page' => 0); 758 $result = testable_mod_quiz_external::validate_attempt($params); 759 $this->assertEquals($attempt->id, $result[0]->get_attempt()->id); 760 $this->assertEquals([], $result[1]); 761 762 // Test with preflight data. 763 $quiz->password = 'abc'; 764 $DB->update_record('quiz', $quiz); 765 766 try { 767 $params = array('attemptid' => $attempt->id, 'page' => 0, 768 'preflightdata' => array(array("name" => "quizpassword", "value" => 'bad'))); 769 testable_mod_quiz_external::validate_attempt($params); 770 $this->fail('Exception expected due to invalid passwod.'); 771 } catch (moodle_exception $e) { 772 $this->assertEquals(get_string('passworderror', 'quizaccess_password'), $e->errorcode); 773 } 774 775 // Now, try everything correct. 776 $params['preflightdata'][0]['value'] = 'abc'; 777 $result = testable_mod_quiz_external::validate_attempt($params); 778 $this->assertEquals($attempt->id, $result[0]->get_attempt()->id); 779 $this->assertEquals([], $result[1]); 780 781 // Page out of range. 782 $DB->update_record('quiz', $quiz); 783 $params['page'] = 4; 784 try { 785 testable_mod_quiz_external::validate_attempt($params); 786 $this->fail('Exception expected due to page out of range.'); 787 } catch (moodle_quiz_exception $e) { 788 $this->assertEquals('Invalid page number', $e->errorcode); 789 } 790 791 $params['page'] = 0; 792 // Try to open attempt in closed quiz. 793 $quiz->timeopen = time() - WEEKSECS; 794 $quiz->timeclose = time() - DAYSECS; 795 $DB->update_record('quiz', $quiz); 796 797 // This should work, ommit access rules. 798 testable_mod_quiz_external::validate_attempt($params, false); 799 800 // Get a generic error because prior to checking the dates the attempt is closed. 801 try { 802 testable_mod_quiz_external::validate_attempt($params); 803 $this->fail('Exception expected due to passed dates.'); 804 } catch (moodle_quiz_exception $e) { 805 $this->assertEquals('attempterror', $e->errorcode); 806 } 807 808 // Finish the attempt. 809 $attemptobj = quiz_attempt::create($attempt->id); 810 $attemptobj->process_finish(time(), false); 811 812 try { 813 testable_mod_quiz_external::validate_attempt($params, false); 814 $this->fail('Exception expected due to attempt finished.'); 815 } catch (moodle_quiz_exception $e) { 816 $this->assertEquals('attemptalreadyclosed', $e->errorcode); 817 } 818 819 // Test user with no capabilities. 820 // We need a explicit prohibit since this capability is only defined in authenticated user and guest roles. 821 assign_capability('mod/quiz:attempt', CAP_PROHIBIT, $this->studentrole->id, $context->id); 822 // Empty all the caches that may be affected by this change. 823 accesslib_clear_all_caches_for_unit_testing(); 824 course_modinfo::clear_instance_cache(); 825 826 try { 827 testable_mod_quiz_external::validate_attempt($params); 828 $this->fail('Exception expected due to missing permissions.'); 829 } catch (required_capability_exception $e) { 830 $this->assertEquals('nopermissions', $e->errorcode); 831 } 832 833 // Now try with a different user. 834 $this->setUser($this->teacher); 835 836 $params['page'] = 0; 837 try { 838 testable_mod_quiz_external::validate_attempt($params); 839 $this->fail('Exception expected due to not your attempt.'); 840 } catch (moodle_quiz_exception $e) { 841 $this->assertEquals('notyourattempt', $e->errorcode); 842 } 843 } 844 845 /** 846 * Test get_attempt_data 847 */ 848 public function test_get_attempt_data() { 849 global $DB; 850 851 // Create a new quiz with one attempt started. 852 list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true); 853 854 $quizobj = $attemptobj->get_quizobj(); 855 $quizobj->preload_questions(); 856 $quizobj->load_questions(); 857 $questions = $quizobj->get_questions(); 858 859 $this->setUser($this->student); 860 861 // We receive one question per page. 862 $result = mod_quiz_external::get_attempt_data($attempt->id, 0); 863 $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_data_returns(), $result); 864 865 $this->assertEquals($attempt, (object) $result['attempt']); 866 $this->assertEquals(1, $result['nextpage']); 867 $this->assertCount(0, $result['messages']); 868 $this->assertCount(1, $result['questions']); 869 $this->assertEquals(1, $result['questions'][0]['slot']); 870 $this->assertEquals(1, $result['questions'][0]['number']); 871 $this->assertEquals('numerical', $result['questions'][0]['type']); 872 $this->assertEquals('todo', $result['questions'][0]['state']); 873 $this->assertEquals(get_string('notyetanswered', 'question'), $result['questions'][0]['status']); 874 $this->assertFalse($result['questions'][0]['flagged']); 875 $this->assertEquals(0, $result['questions'][0]['page']); 876 $this->assertEmpty($result['questions'][0]['mark']); 877 $this->assertEquals(1, $result['questions'][0]['maxmark']); 878 879 // Now try the last page. 880 $result = mod_quiz_external::get_attempt_data($attempt->id, 1); 881 $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_data_returns(), $result); 882 883 $this->assertEquals($attempt, (object) $result['attempt']); 884 $this->assertEquals(-1, $result['nextpage']); 885 $this->assertCount(0, $result['messages']); 886 $this->assertCount(1, $result['questions']); 887 $this->assertEquals(2, $result['questions'][0]['slot']); 888 $this->assertEquals(2, $result['questions'][0]['number']); 889 $this->assertEquals('numerical', $result['questions'][0]['type']); 890 $this->assertEquals('todo', $result['questions'][0]['state']); 891 $this->assertEquals(get_string('notyetanswered', 'question'), $result['questions'][0]['status']); 892 $this->assertFalse($result['questions'][0]['flagged']); 893 $this->assertEquals(1, $result['questions'][0]['page']); 894 895 // Finish previous attempt. 896 $attemptobj->process_finish(time(), false); 897 898 // Change setting and expect two pages. 899 $quiz->questionsperpage = 4; 900 $DB->update_record('quiz', $quiz); 901 quiz_repaginate_questions($quiz->id, $quiz->questionsperpage); 902 903 // Start with new attempt with the new layout. 904 $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); 905 $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); 906 907 $timenow = time(); 908 $attempt = quiz_create_attempt($quizobj, 2, false, $timenow, false, $this->student->id); 909 quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow); 910 quiz_attempt_save_started($quizobj, $quba, $attempt); 911 912 // We receive two questions per page. 913 $result = mod_quiz_external::get_attempt_data($attempt->id, 0); 914 $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_data_returns(), $result); 915 $this->assertCount(2, $result['questions']); 916 $this->assertEquals(-1, $result['nextpage']); 917 918 // Check questions looks good. 919 $found = 0; 920 foreach ($questions as $question) { 921 foreach ($result['questions'] as $rquestion) { 922 if ($rquestion['slot'] == $question->slot) { 923 $this->assertTrue(strpos($rquestion['html'], "qid=$question->id") !== false); 924 $found++; 925 } 926 } 927 } 928 $this->assertEquals(2, $found); 929 930 } 931 932 /** 933 * Test get_attempt_summary 934 */ 935 public function test_get_attempt_summary() { 936 937 // Create a new quiz with one attempt started. 938 list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true); 939 940 $this->setUser($this->student); 941 $result = mod_quiz_external::get_attempt_summary($attempt->id); 942 $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result); 943 944 // Check the state, flagged and mark data is correct. 945 $this->assertEquals('todo', $result['questions'][0]['state']); 946 $this->assertEquals('todo', $result['questions'][1]['state']); 947 $this->assertEquals(1, $result['questions'][0]['number']); 948 $this->assertEquals(2, $result['questions'][1]['number']); 949 $this->assertFalse($result['questions'][0]['flagged']); 950 $this->assertFalse($result['questions'][1]['flagged']); 951 $this->assertEmpty($result['questions'][0]['mark']); 952 $this->assertEmpty($result['questions'][1]['mark']); 953 954 // Submit a response for the first question. 955 $tosubmit = array(1 => array('answer' => '3.14')); 956 $attemptobj->process_submitted_actions(time(), false, $tosubmit); 957 $result = mod_quiz_external::get_attempt_summary($attempt->id); 958 $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result); 959 960 // Check it's marked as completed only the first one. 961 $this->assertEquals('complete', $result['questions'][0]['state']); 962 $this->assertEquals('todo', $result['questions'][1]['state']); 963 $this->assertEquals(1, $result['questions'][0]['number']); 964 $this->assertEquals(2, $result['questions'][1]['number']); 965 $this->assertFalse($result['questions'][0]['flagged']); 966 $this->assertFalse($result['questions'][1]['flagged']); 967 $this->assertEmpty($result['questions'][0]['mark']); 968 $this->assertEmpty($result['questions'][1]['mark']); 969 970 } 971 972 /** 973 * Test save_attempt 974 */ 975 public function test_save_attempt() { 976 977 // Create a new quiz with one attempt started. 978 list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true); 979 980 // Response for slot 1. 981 $prefix = $quba->get_field_prefix(1); 982 $data = array( 983 array('name' => 'slots', 'value' => 1), 984 array('name' => $prefix . ':sequencecheck', 985 'value' => $attemptobj->get_question_attempt(1)->get_sequence_check_count()), 986 array('name' => $prefix . 'answer', 'value' => 1), 987 ); 988 989 $this->setUser($this->student); 990 991 $result = mod_quiz_external::save_attempt($attempt->id, $data); 992 $result = external_api::clean_returnvalue(mod_quiz_external::save_attempt_returns(), $result); 993 $this->assertTrue($result['status']); 994 995 // Now, get the summary. 996 $result = mod_quiz_external::get_attempt_summary($attempt->id); 997 $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result); 998 999 // Check it's marked as completed only the first one. 1000 $this->assertEquals('complete', $result['questions'][0]['state']); 1001 $this->assertEquals('todo', $result['questions'][1]['state']); 1002 $this->assertEquals(1, $result['questions'][0]['number']); 1003 $this->assertEquals(2, $result['questions'][1]['number']); 1004 $this->assertFalse($result['questions'][0]['flagged']); 1005 $this->assertFalse($result['questions'][1]['flagged']); 1006 $this->assertEmpty($result['questions'][0]['mark']); 1007 $this->assertEmpty($result['questions'][1]['mark']); 1008 1009 // Now, second slot. 1010 $prefix = $quba->get_field_prefix(2); 1011 $data = array( 1012 array('name' => 'slots', 'value' => 2), 1013 array('name' => $prefix . ':sequencecheck', 1014 'value' => $attemptobj->get_question_attempt(1)->get_sequence_check_count()), 1015 array('name' => $prefix . 'answer', 'value' => 1), 1016 ); 1017 1018 $result = mod_quiz_external::save_attempt($attempt->id, $data); 1019 $result = external_api::clean_returnvalue(mod_quiz_external::save_attempt_returns(), $result); 1020 $this->assertTrue($result['status']); 1021 1022 // Now, get the summary. 1023 $result = mod_quiz_external::get_attempt_summary($attempt->id); 1024 $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result); 1025 1026 // Check it's marked as completed only the first one. 1027 $this->assertEquals('complete', $result['questions'][0]['state']); 1028 $this->assertEquals('complete', $result['questions'][1]['state']); 1029 1030 } 1031 1032 /** 1033 * Test process_attempt 1034 */ 1035 public function test_process_attempt() { 1036 global $DB; 1037 1038 // Create a new quiz with two questions and one attempt started. 1039 list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true); 1040 1041 // Response for slot 1. 1042 $prefix = $quba->get_field_prefix(1); 1043 $data = array( 1044 array('name' => 'slots', 'value' => 1), 1045 array('name' => $prefix . ':sequencecheck', 1046 'value' => $attemptobj->get_question_attempt(1)->get_sequence_check_count()), 1047 array('name' => $prefix . 'answer', 'value' => 1), 1048 ); 1049 1050 $this->setUser($this->student); 1051 1052 $result = mod_quiz_external::process_attempt($attempt->id, $data); 1053 $result = external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result); 1054 $this->assertEquals(quiz_attempt::IN_PROGRESS, $result['state']); 1055 1056 // Now, get the summary. 1057 $result = mod_quiz_external::get_attempt_summary($attempt->id); 1058 $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result); 1059 1060 // Check it's marked as completed only the first one. 1061 $this->assertEquals('complete', $result['questions'][0]['state']); 1062 $this->assertEquals('todo', $result['questions'][1]['state']); 1063 $this->assertEquals(1, $result['questions'][0]['number']); 1064 $this->assertEquals(2, $result['questions'][1]['number']); 1065 $this->assertFalse($result['questions'][0]['flagged']); 1066 $this->assertFalse($result['questions'][1]['flagged']); 1067 $this->assertEmpty($result['questions'][0]['mark']); 1068 $this->assertEmpty($result['questions'][1]['mark']); 1069 1070 // Now, second slot. 1071 $prefix = $quba->get_field_prefix(2); 1072 $data = array( 1073 array('name' => 'slots', 'value' => 2), 1074 array('name' => $prefix . ':sequencecheck', 1075 'value' => $attemptobj->get_question_attempt(1)->get_sequence_check_count()), 1076 array('name' => $prefix . 'answer', 'value' => 1), 1077 array('name' => $prefix . ':flagged', 'value' => 1), 1078 ); 1079 1080 $result = mod_quiz_external::process_attempt($attempt->id, $data); 1081 $result = external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result); 1082 $this->assertEquals(quiz_attempt::IN_PROGRESS, $result['state']); 1083 1084 // Now, get the summary. 1085 $result = mod_quiz_external::get_attempt_summary($attempt->id); 1086 $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_summary_returns(), $result); 1087 1088 // Check it's marked as completed only the first one. 1089 $this->assertEquals('complete', $result['questions'][0]['state']); 1090 $this->assertEquals('complete', $result['questions'][1]['state']); 1091 $this->assertFalse($result['questions'][0]['flagged']); 1092 $this->assertTrue($result['questions'][1]['flagged']); 1093 1094 // Finish the attempt. 1095 $result = mod_quiz_external::process_attempt($attempt->id, array(), true); 1096 $result = external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result); 1097 $this->assertEquals(quiz_attempt::FINISHED, $result['state']); 1098 1099 // Start new attempt. 1100 $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); 1101 $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); 1102 1103 $timenow = time(); 1104 $attempt = quiz_create_attempt($quizobj, 2, false, $timenow, false, $this->student->id); 1105 quiz_start_new_attempt($quizobj, $quba, $attempt, 2, $timenow); 1106 quiz_attempt_save_started($quizobj, $quba, $attempt); 1107 1108 // Force grace period, attempt going to overdue. 1109 $quiz->timeclose = $timenow - 10; 1110 $quiz->graceperiod = 60; 1111 $quiz->overduehandling = 'graceperiod'; 1112 $DB->update_record('quiz', $quiz); 1113 1114 $result = mod_quiz_external::process_attempt($attempt->id, array()); 1115 $result = external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result); 1116 $this->assertEquals(quiz_attempt::OVERDUE, $result['state']); 1117 1118 // New attempt. 1119 $timenow = time(); 1120 $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); 1121 $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); 1122 $attempt = quiz_create_attempt($quizobj, 3, 2, $timenow, false, $this->student->id); 1123 quiz_start_new_attempt($quizobj, $quba, $attempt, 3, $timenow); 1124 quiz_attempt_save_started($quizobj, $quba, $attempt); 1125 1126 // Force abandon. 1127 $quiz->timeclose = $timenow - HOURSECS; 1128 $DB->update_record('quiz', $quiz); 1129 1130 $result = mod_quiz_external::process_attempt($attempt->id, array()); 1131 $result = external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result); 1132 $this->assertEquals(quiz_attempt::ABANDONED, $result['state']); 1133 1134 } 1135 1136 /** 1137 * Test validate_attempt_review 1138 */ 1139 public function test_validate_attempt_review() { 1140 global $DB; 1141 1142 // Create a new quiz with one attempt started. 1143 list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true); 1144 1145 $this->setUser($this->student); 1146 1147 // Invalid attempt, invalid id. 1148 try { 1149 $params = array('attemptid' => -1); 1150 testable_mod_quiz_external::validate_attempt_review($params); 1151 $this->fail('Exception expected due invalid id.'); 1152 } catch (dml_missing_record_exception $e) { 1153 $this->assertEquals('invalidrecord', $e->errorcode); 1154 } 1155 1156 // Invalid attempt, not closed. 1157 try { 1158 $params = array('attemptid' => $attempt->id); 1159 testable_mod_quiz_external::validate_attempt_review($params); 1160 $this->fail('Exception expected due not closed attempt.'); 1161 } catch (moodle_quiz_exception $e) { 1162 $this->assertEquals('attemptclosed', $e->errorcode); 1163 } 1164 1165 // Test ok case (finished attempt). 1166 list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true, true); 1167 1168 $params = array('attemptid' => $attempt->id); 1169 testable_mod_quiz_external::validate_attempt_review($params); 1170 1171 // Teacher should be able to view the review of one student's attempt. 1172 $this->setUser($this->teacher); 1173 testable_mod_quiz_external::validate_attempt_review($params); 1174 1175 // We should not see other students attempts. 1176 $anotherstudent = self::getDataGenerator()->create_user(); 1177 $this->getDataGenerator()->enrol_user($anotherstudent->id, $this->course->id, $this->studentrole->id, 'manual'); 1178 1179 $this->setUser($anotherstudent); 1180 try { 1181 $params = array('attemptid' => $attempt->id); 1182 testable_mod_quiz_external::validate_attempt_review($params); 1183 $this->fail('Exception expected due missing permissions.'); 1184 } catch (moodle_quiz_exception $e) { 1185 $this->assertEquals('noreviewattempt', $e->errorcode); 1186 } 1187 } 1188 1189 1190 /** 1191 * Test get_attempt_review 1192 */ 1193 public function test_get_attempt_review() { 1194 global $DB; 1195 1196 // Create a new quiz with two questions and one attempt finished. 1197 list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true, true); 1198 1199 // Add feedback to the quiz. 1200 $feedback = new stdClass(); 1201 $feedback->quizid = $quiz->id; 1202 $feedback->feedbacktext = 'Feedback text 1'; 1203 $feedback->feedbacktextformat = 1; 1204 $feedback->mingrade = 49; 1205 $feedback->maxgrade = 100; 1206 $feedback->id = $DB->insert_record('quiz_feedback', $feedback); 1207 1208 $feedback->feedbacktext = 'Feedback text 2'; 1209 $feedback->feedbacktextformat = 1; 1210 $feedback->mingrade = 30; 1211 $feedback->maxgrade = 48; 1212 $feedback->id = $DB->insert_record('quiz_feedback', $feedback); 1213 1214 $result = mod_quiz_external::get_attempt_review($attempt->id); 1215 $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_review_returns(), $result); 1216 1217 // Two questions, one completed and correct, the other gave up. 1218 $this->assertEquals(50, $result['grade']); 1219 $this->assertEquals(1, $result['attempt']['attempt']); 1220 $this->assertEquals('finished', $result['attempt']['state']); 1221 $this->assertEquals(1, $result['attempt']['sumgrades']); 1222 $this->assertCount(2, $result['questions']); 1223 $this->assertEquals('gradedright', $result['questions'][0]['state']); 1224 $this->assertEquals(1, $result['questions'][0]['slot']); 1225 $this->assertEquals('gaveup', $result['questions'][1]['state']); 1226 $this->assertEquals(2, $result['questions'][1]['slot']); 1227 1228 $this->assertCount(1, $result['additionaldata']); 1229 $this->assertEquals('feedback', $result['additionaldata'][0]['id']); 1230 $this->assertEquals('Feedback', $result['additionaldata'][0]['title']); 1231 $this->assertEquals('Feedback text 1', $result['additionaldata'][0]['content']); 1232 1233 // Only first page. 1234 $result = mod_quiz_external::get_attempt_review($attempt->id, 0); 1235 $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_review_returns(), $result); 1236 1237 $this->assertEquals(50, $result['grade']); 1238 $this->assertEquals(1, $result['attempt']['attempt']); 1239 $this->assertEquals('finished', $result['attempt']['state']); 1240 $this->assertEquals(1, $result['attempt']['sumgrades']); 1241 $this->assertCount(1, $result['questions']); 1242 $this->assertEquals('gradedright', $result['questions'][0]['state']); 1243 $this->assertEquals(1, $result['questions'][0]['slot']); 1244 1245 $this->assertCount(1, $result['additionaldata']); 1246 $this->assertEquals('feedback', $result['additionaldata'][0]['id']); 1247 $this->assertEquals('Feedback', $result['additionaldata'][0]['title']); 1248 $this->assertEquals('Feedback text 1', $result['additionaldata'][0]['content']); 1249 1250 } 1251 1252 /** 1253 * Test test_view_attempt 1254 */ 1255 public function test_view_attempt() { 1256 global $DB; 1257 1258 // Create a new quiz with two questions and one attempt started. 1259 list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true, false); 1260 1261 // Test user with full capabilities. 1262 $this->setUser($this->student); 1263 1264 // Trigger and capture the event. 1265 $sink = $this->redirectEvents(); 1266 1267 $result = mod_quiz_external::view_attempt($attempt->id, 0); 1268 $result = external_api::clean_returnvalue(mod_quiz_external::view_attempt_returns(), $result); 1269 $this->assertTrue($result['status']); 1270 1271 $events = $sink->get_events(); 1272 $this->assertCount(1, $events); 1273 $event = array_shift($events); 1274 1275 // Checking that the event contains the expected values. 1276 $this->assertInstanceOf('\mod_quiz\event\attempt_viewed', $event); 1277 $this->assertEquals($context, $event->get_context()); 1278 $this->assertEventContextNotUsed($event); 1279 $this->assertNotEmpty($event->get_name()); 1280 1281 // Now, force the quiz with QUIZ_NAVMETHOD_SEQ (sequencial) navigation method. 1282 $DB->set_field('quiz', 'navmethod', QUIZ_NAVMETHOD_SEQ, array('id' => $quiz->id)); 1283 // Quiz requiring preflightdata. 1284 $DB->set_field('quiz', 'password', 'abcdef', array('id' => $quiz->id)); 1285 $preflightdata = array(array("name" => "quizpassword", "value" => 'abcdef')); 1286 1287 // See next page. 1288 $result = mod_quiz_external::view_attempt($attempt->id, 1, $preflightdata); 1289 $result = external_api::clean_returnvalue(mod_quiz_external::view_attempt_returns(), $result); 1290 $this->assertTrue($result['status']); 1291 1292 $events = $sink->get_events(); 1293 $this->assertCount(2, $events); 1294 1295 // Try to go to previous page. 1296 try { 1297 mod_quiz_external::view_attempt($attempt->id, 0); 1298 $this->fail('Exception expected due to try to see a previous page.'); 1299 } catch (moodle_quiz_exception $e) { 1300 $this->assertEquals('Out of sequence access', $e->errorcode); 1301 } 1302 1303 } 1304 1305 /** 1306 * Test test_view_attempt_summary 1307 */ 1308 public function test_view_attempt_summary() { 1309 global $DB; 1310 1311 // Create a new quiz with two questions and one attempt started. 1312 list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true, false); 1313 1314 // Test user with full capabilities. 1315 $this->setUser($this->student); 1316 1317 // Trigger and capture the event. 1318 $sink = $this->redirectEvents(); 1319 1320 $result = mod_quiz_external::view_attempt_summary($attempt->id); 1321 $result = external_api::clean_returnvalue(mod_quiz_external::view_attempt_summary_returns(), $result); 1322 $this->assertTrue($result['status']); 1323 1324 $events = $sink->get_events(); 1325 $this->assertCount(1, $events); 1326 $event = array_shift($events); 1327 1328 // Checking that the event contains the expected values. 1329 $this->assertInstanceOf('\mod_quiz\event\attempt_summary_viewed', $event); 1330 $this->assertEquals($context, $event->get_context()); 1331 $moodlequiz = new \moodle_url('/mod/quiz/summary.php', array('attempt' => $attempt->id)); 1332 $this->assertEquals($moodlequiz, $event->get_url()); 1333 $this->assertEventContextNotUsed($event); 1334 $this->assertNotEmpty($event->get_name()); 1335 1336 // Quiz requiring preflightdata. 1337 $DB->set_field('quiz', 'password', 'abcdef', array('id' => $quiz->id)); 1338 $preflightdata = array(array("name" => "quizpassword", "value" => 'abcdef')); 1339 1340 $result = mod_quiz_external::view_attempt_summary($attempt->id, $preflightdata); 1341 $result = external_api::clean_returnvalue(mod_quiz_external::view_attempt_summary_returns(), $result); 1342 $this->assertTrue($result['status']); 1343 1344 } 1345 1346 /** 1347 * Test test_view_attempt_summary 1348 */ 1349 public function test_view_attempt_review() { 1350 global $DB; 1351 1352 // Create a new quiz with two questions and one attempt finished. 1353 list($quiz, $context, $quizobj, $attempt, $attemptobj, $quba) = $this->create_quiz_with_questions(true, true); 1354 1355 // Test user with full capabilities. 1356 $this->setUser($this->student); 1357 1358 // Trigger and capture the event. 1359 $sink = $this->redirectEvents(); 1360 1361 $result = mod_quiz_external::view_attempt_review($attempt->id, 0); 1362 $result = external_api::clean_returnvalue(mod_quiz_external::view_attempt_review_returns(), $result); 1363 $this->assertTrue($result['status']); 1364 1365 $events = $sink->get_events(); 1366 $this->assertCount(1, $events); 1367 $event = array_shift($events); 1368 1369 // Checking that the event contains the expected values. 1370 $this->assertInstanceOf('\mod_quiz\event\attempt_reviewed', $event); 1371 $this->assertEquals($context, $event->get_context()); 1372 $moodlequiz = new \moodle_url('/mod/quiz/review.php', array('attempt' => $attempt->id)); 1373 $this->assertEquals($moodlequiz, $event->get_url()); 1374 $this->assertEventContextNotUsed($event); 1375 $this->assertNotEmpty($event->get_name()); 1376 1377 } 1378 1379 /** 1380 * Test get_quiz_feedback_for_grade 1381 */ 1382 public function test_get_quiz_feedback_for_grade() { 1383 global $DB; 1384 1385 // Add feedback to the quiz. 1386 $feedback = new stdClass(); 1387 $feedback->quizid = $this->quiz->id; 1388 $feedback->feedbacktext = 'Feedback text 1'; 1389 $feedback->feedbacktextformat = 1; 1390 $feedback->mingrade = 49; 1391 $feedback->maxgrade = 100; 1392 $feedback->id = $DB->insert_record('quiz_feedback', $feedback); 1393 1394 $feedback->feedbacktext = 'Feedback text 2'; 1395 $feedback->feedbacktextformat = 1; 1396 $feedback->mingrade = 30; 1397 $feedback->maxgrade = 49; 1398 $feedback->id = $DB->insert_record('quiz_feedback', $feedback); 1399 1400 $result = mod_quiz_external::get_quiz_feedback_for_grade($this->quiz->id, 50); 1401 $result = external_api::clean_returnvalue(mod_quiz_external::get_quiz_feedback_for_grade_returns(), $result); 1402 $this->assertEquals('Feedback text 1', $result['feedbacktext']); 1403 $this->assertEquals(FORMAT_HTML, $result['feedbacktextformat']); 1404 1405 $result = mod_quiz_external::get_quiz_feedback_for_grade($this->quiz->id, 30); 1406 $result = external_api::clean_returnvalue(mod_quiz_external::get_quiz_feedback_for_grade_returns(), $result); 1407 $this->assertEquals('Feedback text 2', $result['feedbacktext']); 1408 $this->assertEquals(FORMAT_HTML, $result['feedbacktextformat']); 1409 1410 $result = mod_quiz_external::get_quiz_feedback_for_grade($this->quiz->id, 10); 1411 $result = external_api::clean_returnvalue(mod_quiz_external::get_quiz_feedback_for_grade_returns(), $result); 1412 $this->assertEquals('', $result['feedbacktext']); 1413 $this->assertEquals(FORMAT_MOODLE, $result['feedbacktextformat']); 1414 } 1415 1416 /** 1417 * Test get_quiz_access_information 1418 */ 1419 public function test_get_quiz_access_information() { 1420 global $DB; 1421 1422 // Create a new quiz. 1423 $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); 1424 $data = array('course' => $this->course->id); 1425 $quiz = $quizgenerator->create_instance($data); 1426 1427 $this->setUser($this->student); 1428 1429 // Default restrictions (none). 1430 $result = mod_quiz_external::get_quiz_access_information($quiz->id); 1431 $result = external_api::clean_returnvalue(mod_quiz_external::get_quiz_access_information_returns(), $result); 1432 1433 $expected = array( 1434 'canattempt' => true, 1435 'canmanage' => false, 1436 'canpreview' => false, 1437 'canreviewmyattempts' => true, 1438 'canviewreports' => false, 1439 'accessrules' => [], 1440 // This rule is always used, even if the quiz has no open or close date. 1441 'activerulenames' => ['quizaccess_openclosedate'], 1442 'preventaccessreasons' => [], 1443 'warnings' => [] 1444 ); 1445 1446 $this->assertEquals($expected, $result); 1447 1448 // Now teacher, different privileges. 1449 $this->setUser($this->teacher); 1450 $result = mod_quiz_external::get_quiz_access_information($quiz->id); 1451 $result = external_api::clean_returnvalue(mod_quiz_external::get_quiz_access_information_returns(), $result); 1452 1453 $expected['canmanage'] = true; 1454 $expected['canpreview'] = true; 1455 $expected['canviewreports'] = true; 1456 $expected['canattempt'] = false; 1457 $expected['canreviewmyattempts'] = false; 1458 1459 $this->assertEquals($expected, $result); 1460 1461 $this->setUser($this->student); 1462 // Now add some restrictions. 1463 $quiz->timeopen = time() + DAYSECS; 1464 $quiz->timeclose = time() + WEEKSECS; 1465 $quiz->password = '123456'; 1466 $DB->update_record('quiz', $quiz); 1467 1468 $result = mod_quiz_external::get_quiz_access_information($quiz->id); 1469 $result = external_api::clean_returnvalue(mod_quiz_external::get_quiz_access_information_returns(), $result); 1470 1471 // Access limited by time and password. 1472 $this->assertCount(3, $result['accessrules']); 1473 // Two rule names, password and open/close date. 1474 $this->assertCount(2, $result['activerulenames']); 1475 $this->assertCount(1, $result['preventaccessreasons']); 1476 1477 } 1478 1479 /** 1480 * Test get_attempt_access_information 1481 */ 1482 public function test_get_attempt_access_information() { 1483 global $DB; 1484 1485 // Create a new quiz with attempts. 1486 $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); 1487 $data = array('course' => $this->course->id, 1488 'sumgrades' => 2); 1489 $quiz = $quizgenerator->create_instance($data); 1490 1491 // Create some questions. 1492 $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); 1493 1494 $cat = $questiongenerator->create_question_category(); 1495 $question = $questiongenerator->create_question('numerical', null, array('category' => $cat->id)); 1496 quiz_add_quiz_question($question->id, $quiz); 1497 1498 $question = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id)); 1499 quiz_add_quiz_question($question->id, $quiz); 1500 1501 // Add new question types in the category (for the random one). 1502 $question = $questiongenerator->create_question('truefalse', null, array('category' => $cat->id)); 1503 $question = $questiongenerator->create_question('essay', null, array('category' => $cat->id)); 1504 1505 $question = $questiongenerator->create_question('random', null, array('category' => $cat->id)); 1506 quiz_add_quiz_question($question->id, $quiz); 1507 1508 $quizobj = quiz::create($quiz->id, $this->student->id); 1509 1510 // Set grade to pass. 1511 $item = grade_item::fetch(array('courseid' => $this->course->id, 'itemtype' => 'mod', 1512 'itemmodule' => 'quiz', 'iteminstance' => $quiz->id, 'outcomeid' => null)); 1513 $item->gradepass = 80; 1514 $item->update(); 1515 1516 $this->setUser($this->student); 1517 1518 // Default restrictions (none). 1519 $result = mod_quiz_external::get_attempt_access_information($quiz->id); 1520 $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_access_information_returns(), $result); 1521 1522 $expected = array( 1523 'isfinished' => false, 1524 'preventnewattemptreasons' => [], 1525 'warnings' => [] 1526 ); 1527 1528 $this->assertEquals($expected, $result); 1529 1530 // Limited attempts. 1531 $quiz->attempts = 1; 1532 $DB->update_record('quiz', $quiz); 1533 1534 // Now, do one attempt. 1535 $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); 1536 $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); 1537 1538 $timenow = time(); 1539 $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $this->student->id); 1540 quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow); 1541 quiz_attempt_save_started($quizobj, $quba, $attempt); 1542 1543 // Process some responses from the student. 1544 $attemptobj = quiz_attempt::create($attempt->id); 1545 $tosubmit = array(1 => array('answer' => '3.14')); 1546 $attemptobj->process_submitted_actions($timenow, false, $tosubmit); 1547 1548 // Finish the attempt. 1549 $attemptobj = quiz_attempt::create($attempt->id); 1550 $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question()); 1551 $attemptobj->process_finish($timenow, false); 1552 1553 // Can we start a new attempt? We shall not! 1554 $result = mod_quiz_external::get_attempt_access_information($quiz->id, $attempt->id); 1555 $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_access_information_returns(), $result); 1556 1557 // Now new attemps allowed. 1558 $this->assertCount(1, $result['preventnewattemptreasons']); 1559 $this->assertFalse($result['ispreflightcheckrequired']); 1560 $this->assertEquals(get_string('nomoreattempts', 'quiz'), $result['preventnewattemptreasons'][0]); 1561 1562 } 1563 1564 /** 1565 * Test get_quiz_required_qtypes 1566 */ 1567 public function test_get_quiz_required_qtypes() { 1568 global $DB; 1569 1570 // Create a new quiz. 1571 $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); 1572 $data = array('course' => $this->course->id); 1573 $quiz = $quizgenerator->create_instance($data); 1574 1575 // Create some questions. 1576 $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); 1577 1578 $cat = $questiongenerator->create_question_category(); 1579 $question = $questiongenerator->create_question('numerical', null, array('category' => $cat->id)); 1580 quiz_add_quiz_question($question->id, $quiz); 1581 1582 $question = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id)); 1583 quiz_add_quiz_question($question->id, $quiz); 1584 1585 // Add new question types in the category (for the random one). 1586 $question = $questiongenerator->create_question('truefalse', null, array('category' => $cat->id)); 1587 $question = $questiongenerator->create_question('essay', null, array('category' => $cat->id)); 1588 1589 $question = $questiongenerator->create_question('random', null, array('category' => $cat->id)); 1590 quiz_add_quiz_question($question->id, $quiz); 1591 1592 $this->setUser($this->student); 1593 1594 $result = mod_quiz_external::get_quiz_required_qtypes($quiz->id); 1595 $result = external_api::clean_returnvalue(mod_quiz_external::get_quiz_required_qtypes_returns(), $result); 1596 1597 $expected = array( 1598 'questiontypes' => ['essay', 'numerical', 'random', 'shortanswer', 'truefalse'], 1599 'warnings' => [] 1600 ); 1601 1602 $this->assertEquals($expected, $result); 1603 1604 } 1605 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Aug 11 10:00:09 2016 | Cross-referenced by PHPXref 0.7.1 |