Differences Between: [Versions 400 and 402] [Versions 401 and 402] [Versions 402 and 403]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 namespace mod_quiz; 18 19 defined('MOODLE_INTERNAL') || die(); 20 21 global $CFG; 22 require_once (__DIR__ . '/quiz_question_helper_test_trait.php'); 23 require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); 24 require_once($CFG->dirroot . '/mod/quiz/locallib.php'); 25 26 /** 27 * Quiz backup and restore tests. 28 * 29 * @package mod_quiz 30 * @category test 31 * @copyright 2021 Catalyst IT Australia Pty Ltd 32 * @author Safat Shahin <safatshahin@catalyst-au.net> 33 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 34 * @coversDefaultClass \mod_quiz\question\bank\qbank_helper 35 * @coversDefaultClass \backup_quiz_activity_structure_step 36 * @coversDefaultClass \restore_quiz_activity_structure_step 37 */ 38 class quiz_question_restore_test extends \advanced_testcase { 39 use \quiz_question_helper_test_trait; 40 41 /** 42 * @var \stdClass test student user. 43 */ 44 protected $student; 45 46 /** 47 * Called before every test. 48 */ 49 public function setUp(): void { 50 global $USER; 51 parent::setUp(); 52 $this->setAdminUser(); 53 $this->course = $this->getDataGenerator()->create_course(); 54 $this->student = $this->getDataGenerator()->create_user(); 55 $this->user = $USER; 56 } 57 58 /** 59 * Test a quiz backup and restore in a different course without attempts for course question bank. 60 * 61 * @covers ::get_question_structure 62 */ 63 public function test_quiz_restore_in_a_different_course_using_course_question_bank() { 64 $this->resetAfterTest(); 65 66 // Create the test quiz. 67 $quiz = $this->create_test_quiz($this->course); 68 $oldquizcontext = \context_module::instance($quiz->cmid); 69 // Test for questions from a different context. 70 $coursecontext = \context_course::instance($this->course->id); 71 $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); 72 $this->add_two_regular_questions($questiongenerator, $quiz, ['contextid' => $coursecontext->id]); 73 $this->add_one_random_question($questiongenerator, $quiz, ['contextid' => $coursecontext->id]); 74 75 // Make the backup. 76 $backupid = $this->backup_quiz($quiz, $this->user); 77 78 // Delete the current course to make sure there is no data. 79 delete_course($this->course, false); 80 81 // Check if the questions and associated data are deleted properly. 82 $this->assertEquals(0, count(\mod_quiz\question\bank\qbank_helper::get_question_structure( 83 $quiz->id, $oldquizcontext))); 84 85 // Restore the course. 86 $newcourse = $this->getDataGenerator()->create_course(); 87 $this->restore_quiz($backupid, $newcourse, $this->user); 88 89 // Verify. 90 $modules = get_fast_modinfo($newcourse->id)->get_instances_of('quiz'); 91 $module = reset($modules); 92 $questions = \mod_quiz\question\bank\qbank_helper::get_question_structure( 93 $module->instance, $module->context); 94 $this->assertCount(3, $questions); 95 } 96 97 /** 98 * Test a quiz backup and restore in a different course without attempts for quiz question bank. 99 * 100 * @covers ::get_question_structure 101 */ 102 public function test_quiz_restore_in_a_different_course_using_quiz_question_bank() { 103 $this->resetAfterTest(); 104 105 // Create the test quiz. 106 $quiz = $this->create_test_quiz($this->course); 107 // Test for questions from a different context. 108 $quizcontext = \context_module::instance($quiz->cmid); 109 $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); 110 $this->add_two_regular_questions($questiongenerator, $quiz, ['contextid' => $quizcontext->id]); 111 $this->add_one_random_question($questiongenerator, $quiz, ['contextid' => $quizcontext->id]); 112 113 // Make the backup. 114 $backupid = $this->backup_quiz($quiz, $this->user); 115 116 // Delete the current course to make sure there is no data. 117 delete_course($this->course, false); 118 119 // Check if the questions and associated datas are deleted properly. 120 $this->assertEquals(0, count(\mod_quiz\question\bank\qbank_helper::get_question_structure( 121 $quiz->id, $quizcontext))); 122 123 // Restore the course. 124 $newcourse = $this->getDataGenerator()->create_course(); 125 $this->restore_quiz($backupid, $newcourse, $this->user); 126 127 // Verify. 128 $modules = get_fast_modinfo($newcourse->id)->get_instances_of('quiz'); 129 $module = reset($modules); 130 $this->assertEquals(3, count(\mod_quiz\question\bank\qbank_helper::get_question_structure( 131 $module->instance, $module->context))); 132 } 133 134 /** 135 * Count the questions for the context. 136 * 137 * @param int $contextid 138 * @param string $extracondition 139 * @return int the number of questions. 140 */ 141 protected function question_count(int $contextid, string $extracondition = ''): int { 142 global $DB; 143 return $DB->count_records_sql( 144 "SELECT COUNT(q.id) 145 FROM {question} q 146 JOIN {question_versions} qv ON qv.questionid = q.id 147 JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid 148 JOIN {question_categories} qc on qc.id = qbe.questioncategoryid 149 WHERE qc.contextid = ? 150 $extracondition", [$contextid]); 151 } 152 153 /** 154 * Test if a duplicate does not duplicate questions in course question bank. 155 * 156 * @covers ::duplicate_module 157 */ 158 public function test_quiz_duplicate_does_not_duplicate_course_question_bank_questions() { 159 $this->resetAfterTest(); 160 $quiz = $this->create_test_quiz($this->course); 161 // Test for questions from a different context. 162 $context = \context_course::instance($this->course->id); 163 $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); 164 $this->add_two_regular_questions($questiongenerator, $quiz, ['contextid' => $context->id]); 165 $this->add_one_random_question($questiongenerator, $quiz, ['contextid' => $context->id]); 166 // Count the questions in course context. 167 $this->assertEquals(7, $this->question_count($context->id)); 168 $newquiz = $this->duplicate_quiz($this->course, $quiz); 169 $this->assertEquals(7, $this->question_count($context->id)); 170 $context = \context_module::instance($newquiz->id); 171 // Count the questions in the quiz context. 172 $this->assertEquals(0, $this->question_count($context->id)); 173 } 174 175 /** 176 * Test quiz duplicate for quiz question bank. 177 * 178 * @covers ::duplicate_module 179 */ 180 public function test_quiz_duplicate_for_quiz_question_bank_questions() { 181 $this->resetAfterTest(); 182 $quiz = $this->create_test_quiz($this->course); 183 // Test for questions from a different context. 184 $context = \context_module::instance($quiz->cmid); 185 $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); 186 $this->add_two_regular_questions($questiongenerator, $quiz, ['contextid' => $context->id]); 187 $this->add_one_random_question($questiongenerator, $quiz, ['contextid' => $context->id]); 188 // Count the questions in course context. 189 $this->assertEquals(7, $this->question_count($context->id)); 190 $newquiz = $this->duplicate_quiz($this->course, $quiz); 191 $this->assertEquals(7, $this->question_count($context->id)); 192 $context = \context_module::instance($newquiz->id); 193 // Count the questions in the quiz context. 194 $this->assertEquals(7, $this->question_count($context->id)); 195 } 196 197 /** 198 * Test quiz restore with attempts. 199 * 200 * @covers ::get_question_structure 201 */ 202 public function test_quiz_restore_with_attempts() { 203 $this->resetAfterTest(); 204 205 // Create a quiz. 206 $quiz = $this->create_test_quiz($this->course); 207 $quizcontext = \context_module::instance($quiz->cmid); 208 $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); 209 $this->add_two_regular_questions($questiongenerator, $quiz, ['contextid' => $quizcontext->id]); 210 $this->add_one_random_question($questiongenerator, $quiz, ['contextid' => $quizcontext->id]); 211 212 // Attempt it as a student, and check. 213 /** @var \question_usage_by_activity $quba */ 214 [, $quba] = $this->attempt_quiz($quiz, $this->student); 215 $this->assertEquals(3, $quba->question_count()); 216 $this->assertCount(1, quiz_get_user_attempts($quiz->id, $this->student->id)); 217 218 // Make the backup. 219 $backupid = $this->backup_quiz($quiz, $this->user); 220 221 // Delete the current course to make sure there is no data. 222 delete_course($this->course, false); 223 224 // Restore the backup. 225 $newcourse = $this->getDataGenerator()->create_course(); 226 $this->restore_quiz($backupid, $newcourse, $this->user); 227 228 // Verify. 229 $modules = get_fast_modinfo($newcourse->id)->get_instances_of('quiz'); 230 $module = reset($modules); 231 $this->assertCount(1, quiz_get_user_attempts($module->instance, $this->student->id)); 232 $this->assertCount(3, \mod_quiz\question\bank\qbank_helper::get_question_structure( 233 $module->instance, $module->context)); 234 } 235 236 /** 237 * Test pre 4.0 quiz restore for regular questions. 238 * 239 * @covers ::process_quiz_question_legacy_instance 240 */ 241 public function test_pre_4_quiz_restore_for_regular_questions() { 242 global $USER, $DB; 243 $this->resetAfterTest(); 244 $backupid = 'abc'; 245 $backuppath = make_backup_temp_directory($backupid); 246 get_file_packer('application/vnd.moodle.backup')->extract_to_pathname( 247 __DIR__ . "/fixtures/moodle_28_quiz.mbz", $backuppath); 248 249 // Do the restore to new course with default settings. 250 $categoryid = $DB->get_field_sql("SELECT MIN(id) FROM {course_categories}"); 251 $newcourseid = \restore_dbops::create_new_course('Test fullname', 'Test shortname', $categoryid); 252 $rc = new \restore_controller($backupid, $newcourseid, \backup::INTERACTIVE_NO, \backup::MODE_GENERAL, $USER->id, 253 \backup::TARGET_NEW_COURSE); 254 255 $this->assertTrue($rc->execute_precheck()); 256 $rc->execute_plan(); 257 $rc->destroy(); 258 259 // Get the information about the resulting course and check that it is set up correctly. 260 $modinfo = get_fast_modinfo($newcourseid); 261 $quiz = array_values($modinfo->get_instances_of('quiz'))[0]; 262 $quizobj = \mod_quiz\quiz_settings::create($quiz->instance); 263 $structure = structure::create_for_quiz($quizobj); 264 265 // Are the correct slots returned? 266 $slots = $structure->get_slots(); 267 $this->assertCount(2, $slots); 268 269 $quizobj->preload_questions(); 270 $quizobj->load_questions(); 271 $questions = $quizobj->get_questions(); 272 $this->assertCount(2, $questions); 273 274 // Count the questions in quiz qbank. 275 $this->assertEquals(2, $this->question_count($quizobj->get_context()->id)); 276 } 277 278 /** 279 * Test pre 4.0 quiz restore for random questions. 280 * 281 * @covers ::process_quiz_question_legacy_instance 282 */ 283 public function test_pre_4_quiz_restore_for_random_questions() { 284 global $USER, $DB; 285 $this->resetAfterTest(); 286 287 $backupid = 'abc'; 288 $backuppath = make_backup_temp_directory($backupid); 289 get_file_packer('application/vnd.moodle.backup')->extract_to_pathname( 290 __DIR__ . "/fixtures/random_by_tag_quiz.mbz", $backuppath); 291 292 // Do the restore to new course with default settings. 293 $categoryid = $DB->get_field_sql("SELECT MIN(id) FROM {course_categories}"); 294 $newcourseid = \restore_dbops::create_new_course('Test fullname', 'Test shortname', $categoryid); 295 $rc = new \restore_controller($backupid, $newcourseid, \backup::INTERACTIVE_NO, \backup::MODE_GENERAL, $USER->id, 296 \backup::TARGET_NEW_COURSE); 297 298 $this->assertTrue($rc->execute_precheck()); 299 $rc->execute_plan(); 300 $rc->destroy(); 301 302 // Get the information about the resulting course and check that it is set up correctly. 303 $modinfo = get_fast_modinfo($newcourseid); 304 $quiz = array_values($modinfo->get_instances_of('quiz'))[0]; 305 $quizobj = \mod_quiz\quiz_settings::create($quiz->instance); 306 $structure = structure::create_for_quiz($quizobj); 307 308 // Are the correct slots returned? 309 $slots = $structure->get_slots(); 310 $this->assertCount(1, $slots); 311 312 $quizobj->preload_questions(); 313 $quizobj->load_questions(); 314 $questions = $quizobj->get_questions(); 315 $this->assertCount(1, $questions); 316 317 // Count the questions for course question bank. 318 $this->assertEquals(6, $this->question_count(\context_course::instance($newcourseid)->id)); 319 $this->assertEquals(6, $this->question_count(\context_course::instance($newcourseid)->id, 320 "AND q.qtype <> 'random'")); 321 322 // Count the questions in quiz qbank. 323 $this->assertEquals(0, $this->question_count($quizobj->get_context()->id)); 324 } 325 326 /** 327 * Test pre 4.0 quiz restore for random question tags. 328 * 329 * @covers ::process_quiz_question_legacy_instance 330 */ 331 public function test_pre_4_quiz_restore_for_random_question_tags() { 332 global $USER, $DB; 333 $this->resetAfterTest(); 334 $randomtags = [ 335 '1' => ['first question', 'one', 'number one'], 336 '2' => ['first question', 'one', 'number one'], 337 '3' => ['one', 'number one', 'second question'], 338 ]; 339 $backupid = 'abc'; 340 $backuppath = make_backup_temp_directory($backupid); 341 get_file_packer('application/vnd.moodle.backup')->extract_to_pathname( 342 __DIR__ . "/fixtures/moodle_311_quiz.mbz", $backuppath); 343 344 // Do the restore to new course with default settings. 345 $categoryid = $DB->get_field_sql("SELECT MIN(id) FROM {course_categories}"); 346 $newcourseid = \restore_dbops::create_new_course('Test fullname', 'Test shortname', $categoryid); 347 $rc = new \restore_controller($backupid, $newcourseid, \backup::INTERACTIVE_NO, \backup::MODE_GENERAL, $USER->id, 348 \backup::TARGET_NEW_COURSE); 349 350 $this->assertTrue($rc->execute_precheck()); 351 $rc->execute_plan(); 352 $rc->destroy(); 353 354 // Get the information about the resulting course and check that it is set up correctly. 355 $modinfo = get_fast_modinfo($newcourseid); 356 $quiz = array_values($modinfo->get_instances_of('quiz'))[0]; 357 $quizobj = \mod_quiz\quiz_settings::create($quiz->instance); 358 $structure = \mod_quiz\structure::create_for_quiz($quizobj); 359 360 // Count the questions in quiz qbank. 361 $context = \context_module::instance(get_coursemodule_from_instance("quiz", $quizobj->get_quizid(), $newcourseid)->id); 362 $this->assertEquals(2, $this->question_count($context->id)); 363 364 // Are the correct slots returned? 365 $slots = $structure->get_slots(); 366 $this->assertCount(3, $slots); 367 368 // Check if the tags match with the actual restored data. 369 foreach ($slots as $slot) { 370 $setreference = $DB->get_record('question_set_references', 371 ['itemid' => $slot->id, 'component' => 'mod_quiz', 'questionarea' => 'slot']); 372 $filterconditions = json_decode($setreference->filtercondition); 373 $tags = []; 374 foreach ($filterconditions->tags as $tagstring) { 375 $tag = explode(',', $tagstring); 376 $tags[] = $tag[1]; 377 } 378 $this->assertEquals([], array_diff($randomtags[$slot->slot], $tags)); 379 } 380 381 } 382 383 /** 384 * Test pre 4.0 quiz restore for random question used on multiple quizzes. 385 * 386 * @covers ::process_quiz_question_legacy_instance 387 */ 388 public function test_pre_4_quiz_restore_shared_random_question() { 389 global $USER, $DB; 390 $this->resetAfterTest(); 391 392 $backupid = 'abc'; 393 $backuppath = make_backup_temp_directory($backupid); 394 get_file_packer('application/vnd.moodle.backup')->extract_to_pathname( 395 __DIR__ . "/fixtures/pre-40-shared-random-question.mbz", $backuppath); 396 397 // Do the restore to new course with default settings. 398 $categoryid = $DB->get_field_sql("SELECT MIN(id) FROM {course_categories}"); 399 $newcourseid = \restore_dbops::create_new_course('Test fullname', 'Test shortname', $categoryid); 400 $rc = new \restore_controller($backupid, $newcourseid, \backup::INTERACTIVE_NO, \backup::MODE_GENERAL, $USER->id, 401 \backup::TARGET_NEW_COURSE); 402 403 $this->assertTrue($rc->execute_precheck()); 404 $rc->execute_plan(); 405 $rc->destroy(); 406 407 // Get the information about the resulting course and check that it is set up correctly. 408 // Each quiz should contain an instance of the random question. 409 $modinfo = get_fast_modinfo($newcourseid); 410 $quizzes = $modinfo->get_instances_of('quiz'); 411 $this->assertCount(2, $quizzes); 412 foreach ($quizzes as $quiz) { 413 $quizobj = \mod_quiz\quiz_settings::create($quiz->instance); 414 $structure = structure::create_for_quiz($quizobj); 415 416 // Are the correct slots returned? 417 $slots = $structure->get_slots(); 418 $this->assertCount(1, $slots); 419 420 $quizobj->preload_questions(); 421 $quizobj->load_questions(); 422 $questions = $quizobj->get_questions(); 423 $this->assertCount(1, $questions); 424 } 425 426 // Count the questions for course question bank. 427 // We should have a single question, the random question should have been deleted after the restore. 428 $this->assertEquals(1, $this->question_count(\context_course::instance($newcourseid)->id)); 429 $this->assertEquals(1, $this->question_count(\context_course::instance($newcourseid)->id, 430 "AND q.qtype <> 'random'")); 431 432 // Count the questions in quiz qbank. 433 $this->assertEquals(0, $this->question_count($quizobj->get_context()->id)); 434 } 435 436 /** 437 * Ensure that question slots are correctly backed up and restored with all properties. 438 * 439 * @covers \backup_quiz_activity_structure_step::define_structure() 440 * @return void 441 */ 442 public function test_backup_restore_question_slots(): void { 443 $this->resetAfterTest(true); 444 445 $course1 = $this->getDataGenerator()->create_course(); 446 $course2 = $this->getDataGenerator()->create_course(); 447 448 $user1 = $this->getDataGenerator()->create_and_enrol($course1, 'editingteacher'); 449 $this->getDataGenerator()->enrol_user($user1->id, $course2->id, 'editingteacher'); 450 451 // Make a quiz. 452 $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); 453 454 $quiz = $quizgenerator->create_instance(['course' => $course1->id, 'questionsperpage' => 0, 'grade' => 100.0, 455 'sumgrades' => 3]); 456 457 // Create some fixed and random questions. 458 $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); 459 460 $cat = $questiongenerator->create_question_category(); 461 $saq = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]); 462 $numq = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]); 463 $matchq = $questiongenerator->create_question('match', null, ['category' => $cat->id]); 464 $randomcat = $questiongenerator->create_question_category(); 465 $questiongenerator->create_question('shortanswer', null, ['category' => $randomcat->id]); 466 $questiongenerator->create_question('numerical', null, ['category' => $randomcat->id]); 467 $questiongenerator->create_question('match', null, ['category' => $randomcat->id]); 468 469 // Add them to the quiz. 470 quiz_add_quiz_question($saq->id, $quiz, 1, 3); 471 quiz_add_quiz_question($numq->id, $quiz, 2, 2); 472 quiz_add_quiz_question($matchq->id, $quiz, 3, 1); 473 quiz_add_random_questions($quiz, 3, $randomcat->id, 2, false); 474 475 $quizobj = quiz_settings::create($quiz->id, $user1->id); 476 $originalstructure = \mod_quiz\structure::create_for_quiz($quizobj); 477 478 // Set one slot to a non-default display number. 479 $originalslots = $originalstructure->get_slots(); 480 $firstslot = reset($originalslots); 481 $originalstructure->update_slot_display_number($firstslot->id, rand(5, 10)); 482 483 // Set one slot to requireprevious. 484 $lastslot = end($originalslots); 485 $originalstructure->update_question_dependency($lastslot->id, true); 486 487 // Backup and restore the quiz. 488 $backupid = $this->backup_quiz($quiz, $user1); 489 $this->restore_quiz($backupid, $course2, $user1); 490 491 // Ensure the restored slots match the original slots. 492 $modinfo = get_fast_modinfo($course2); 493 $quizzes = $modinfo->get_instances_of('quiz'); 494 $restoredquiz = reset($quizzes); 495 $restoredquizobj = quiz_settings::create($restoredquiz->instance, $user1->id); 496 $restoredstructure = \mod_quiz\structure::create_for_quiz($restoredquizobj); 497 $restoredslots = array_values($restoredstructure->get_slots()); 498 $originalstructure = \mod_quiz\structure::create_for_quiz($quizobj); 499 $originalslots = array_values($originalstructure->get_slots()); 500 foreach ($restoredslots as $key => $restoredslot) { 501 $originalslot = $originalslots[$key]; 502 $this->assertEquals($originalslot->quizid, $quiz->id); 503 $this->assertEquals($restoredslot->quizid, $restoredquiz->instance); 504 $this->assertEquals($originalslot->slot, $restoredslot->slot); 505 $this->assertEquals($originalslot->page, $restoredslot->page); 506 $this->assertEquals($originalslot->displaynumber, $restoredslot->displaynumber); 507 $this->assertEquals($originalslot->requireprevious, $restoredslot->requireprevious); 508 $this->assertEquals($originalslot->maxmark, $restoredslot->maxmark); 509 } 510 } 511 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body