See Release Notes
Long Term Support Release
Differences Between: [Versions 39 and 310] [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * Unit tests for question backup and restore. 19 * 20 * @package core_question 21 * @category test 22 * @copyright 2018 Shamim Rezaie <shamim@moodle.com> 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 */ 25 26 defined('MOODLE_INTERNAL') || die(); 27 28 global $CFG; 29 require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); 30 require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); 31 32 /** 33 * Class core_question_backup_testcase 34 * 35 * @copyright 2018 Shamim Rezaie <shamim@moodle.com> 36 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 37 */ 38 class core_question_backup_testcase extends advanced_testcase { 39 40 /** 41 * Makes a backup of the course. 42 * 43 * @param stdClass $course The course object. 44 * @return string Unique identifier for this backup. 45 */ 46 protected function backup_course($course) { 47 global $CFG, $USER; 48 49 // Turn off file logging, otherwise it can't delete the file (Windows). 50 $CFG->backup_file_logger_level = backup::LOG_NONE; 51 52 // Do backup with default settings. MODE_IMPORT means it will just 53 // create the directory and not zip it. 54 $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, 55 backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, 56 $USER->id); 57 $backupid = $bc->get_backupid(); 58 $bc->execute_plan(); 59 $bc->destroy(); 60 61 return $backupid; 62 } 63 64 /** 65 * Restores a backup that has been made earlier. 66 * 67 * @param string $backupid The unique identifier of the backup. 68 * @param string $fullname Full name of the new course that is going to be created. 69 * @param string $shortname Short name of the new course that is going to be created. 70 * @param int $categoryid The course category the backup is going to be restored in. 71 * @param string[] $expectedprecheckwarning 72 * @return int The new course id. 73 */ 74 protected function restore_course($backupid, $fullname, $shortname, $categoryid, $expectedprecheckwarning = []) { 75 global $CFG, $USER; 76 77 // Turn off file logging, otherwise it can't delete the file (Windows). 78 $CFG->backup_file_logger_level = backup::LOG_NONE; 79 80 // Do restore to new course with default settings. 81 $newcourseid = restore_dbops::create_new_course($fullname, $shortname, $categoryid); 82 $rc = new restore_controller($backupid, $newcourseid, 83 backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, 84 backup::TARGET_NEW_COURSE); 85 86 $precheck = $rc->execute_precheck(); 87 if (!$expectedprecheckwarning) { 88 $this->assertTrue($precheck); 89 } else { 90 $precheckresults = $rc->get_precheck_results(); 91 $this->assertEqualsCanonicalizing($expectedprecheckwarning, $precheckresults['warnings']); 92 $this->assertCount(1, $precheckresults); 93 } 94 $rc->execute_plan(); 95 $rc->destroy(); 96 97 return $newcourseid; 98 } 99 100 /** 101 * This function tests backup and restore of question tags and course level question tags. 102 */ 103 public function test_backup_question_tags() { 104 global $DB; 105 106 $this->resetAfterTest(); 107 $this->setAdminUser(); 108 109 // Create a new course category and and a new course in that. 110 $category1 = $this->getDataGenerator()->create_category(); 111 $course = $this->getDataGenerator()->create_course(array('category' => $category1->id)); 112 $courseshortname = $course->shortname; 113 $coursefullname = $course->fullname; 114 115 // Create 2 questions. 116 $qgen = $this->getDataGenerator()->get_plugin_generator('core_question'); 117 $context = context_coursecat::instance($category1->id); 118 $qcat = $qgen->create_question_category(array('contextid' => $context->id)); 119 $question1 = $qgen->create_question('shortanswer', null, array('category' => $qcat->id, 'idnumber' => 'q1')); 120 $question2 = $qgen->create_question('shortanswer', null, array('category' => $qcat->id, 'idnumber' => 'q2')); 121 122 // Tag the questions with 2 question tags and 2 course level question tags. 123 $qcontext = context::instance_by_id($qcat->contextid); 124 $coursecontext = context_course::instance($course->id); 125 core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['qtag1', 'qtag2']); 126 core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['qtag3', 'qtag4']); 127 core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['ctag1', 'ctag2']); 128 core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['ctag3', 'ctag4']); 129 130 // Create a quiz and add one of the questions to that. 131 $quiz = $this->getDataGenerator()->create_module('quiz', array('course' => $course->id)); 132 quiz_add_quiz_question($question1->id, $quiz); 133 134 // Backup the course twice for future use. 135 $backupid1 = $this->backup_course($course); 136 $backupid2 = $this->backup_course($course); 137 138 // Now delete almost everything. 139 delete_course($course, false); 140 question_delete_question($question1->id); 141 question_delete_question($question2->id); 142 143 // Restore the backup we had made earlier into a new course. 144 $courseid2 = $this->restore_course($backupid1, $coursefullname, $courseshortname . '_2', $category1->id); 145 146 // The questions should remain in the question category they were which is 147 // a question category belonging to a course category context. 148 $questions = $DB->get_records('question', array('category' => $qcat->id), 'idnumber'); 149 $this->assertCount(2, $questions); 150 151 // Retrieve tags for each question and check if they are assigned at the right context. 152 $qcount = 1; 153 foreach ($questions as $question) { 154 $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id); 155 156 // Each question is tagged with 4 tags (2 question tags + 2 course tags). 157 $this->assertCount(4, $tags); 158 159 foreach ($tags as $tag) { 160 if (in_array($tag->name, ['ctag1', 'ctag2', 'ctag3', 'ctag4'])) { 161 $expected = context_course::instance($courseid2)->id; 162 } else if (in_array($tag->name, ['qtag1', 'qtag2', 'qtag3', 'qtag4'])) { 163 $expected = $qcontext->id; 164 } 165 $this->assertEquals($expected, $tag->taginstancecontextid); 166 } 167 168 // Also check idnumbers have been backed up and restored. 169 $this->assertEquals('q' . $qcount, $question->idnumber); 170 $qcount++; 171 } 172 173 // Now, again, delete everything including the course category. 174 delete_course($courseid2, false); 175 foreach ($questions as $question) { 176 question_delete_question($question->id); 177 } 178 $category1->delete_full(false); 179 180 // Create a new course category to restore the backup file into it. 181 $category2 = $this->getDataGenerator()->create_category(); 182 183 $expectedwarnings = array( 184 get_string('qcategory2coursefallback', 'backup', (object) ['name' => 'top']), 185 get_string('qcategory2coursefallback', 'backup', (object) ['name' => $qcat->name]) 186 ); 187 188 // Restore to a new course in the new course category. 189 $courseid3 = $this->restore_course($backupid2, $coursefullname, $courseshortname . '_3', $category2->id, $expectedwarnings); 190 $coursecontext3 = context_course::instance($courseid3); 191 192 // The questions should have been moved to a question category that belongs to a course context. 193 $questions = $DB->get_records_sql("SELECT q.* 194 FROM {question} q 195 JOIN {question_categories} qc ON q.category = qc.id 196 WHERE qc.contextid = ?", array($coursecontext3->id)); 197 $this->assertCount(2, $questions); 198 199 // Now, retrieve tags for each question and check if they are assigned at the right context. 200 foreach ($questions as $question) { 201 $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id); 202 203 // Each question is tagged with 4 tags (all are course tags now). 204 $this->assertCount(4, $tags); 205 206 foreach ($tags as $tag) { 207 $this->assertEquals($coursecontext3->id, $tag->taginstancecontextid); 208 } 209 } 210 211 } 212 213 /** 214 * Test that the question author is retained when they are enrolled in to the course. 215 */ 216 public function test_backup_question_author_retained_when_enrolled() { 217 global $DB, $USER, $CFG; 218 $this->resetAfterTest(); 219 $this->setAdminUser(); 220 221 // Create a course, a category and a user. 222 $course = $this->getDataGenerator()->create_course(); 223 $category = $this->getDataGenerator()->create_category(); 224 $user = $this->getDataGenerator()->create_user(); 225 226 // Create a question. 227 $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); 228 $questioncategory = $questiongenerator->create_question_category(); 229 $overrides = ['name' => 'Test question', 'category' => $questioncategory->id, 230 'createdby' => $user->id, 'modifiedby' => $user->id]; 231 $question = $questiongenerator->create_question('truefalse', null, $overrides); 232 233 // Create a quiz and a questions. 234 $quiz = $this->getDataGenerator()->create_module('quiz', array('course' => $course->id)); 235 quiz_add_quiz_question($question->id, $quiz); 236 237 // Enrol user with a teacher role. 238 $teacherrole = $DB->get_record('role', ['shortname' => 'editingteacher']); 239 $this->getDataGenerator()->enrol_user($user->id, $course->id, $teacherrole->id, 'manual'); 240 241 // Backup the course. 242 $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE, 243 backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id); 244 $backupid = $bc->get_backupid(); 245 $bc->execute_plan(); 246 $results = $bc->get_results(); 247 $file = $results['backup_destination']; 248 $fp = get_file_packer('application/vnd.moodle.backup'); 249 $filepath = $CFG->dataroot . '/temp/backup/' . $backupid; 250 $file->extract_to_pathname($fp, $filepath); 251 $bc->destroy(); 252 253 // Delete the original course and related question. 254 delete_course($course, false); 255 question_delete_question($question->id); 256 257 // Restore the course. 258 $restoredcourseid = restore_dbops::create_new_course($course->fullname, $course->shortname . '_1', $category->id); 259 $rc = new restore_controller($backupid, $restoredcourseid, backup::INTERACTIVE_NO, 260 backup::MODE_GENERAL, $USER->id, backup::TARGET_NEW_COURSE); 261 $rc->execute_precheck(); 262 $rc->execute_plan(); 263 $rc->destroy(); 264 265 // Test the question author. 266 $questions = $DB->get_records('question', ['name' => 'Test question']); 267 $this->assertCount(1, $questions); 268 $question3 = array_shift($questions); 269 $this->assertEquals($user->id, $question3->createdby); 270 $this->assertEquals($user->id, $question3->modifiedby); 271 } 272 273 /** 274 * Test that the question author is retained when they are not enrolled in to the course, 275 * but we are restoring the backup at the same site. 276 */ 277 public function test_backup_question_author_retained_when_not_enrolled() { 278 global $DB, $USER, $CFG; 279 $this->resetAfterTest(); 280 $this->setAdminUser(); 281 282 // Create a course, a category and a user. 283 $course = $this->getDataGenerator()->create_course(); 284 $category = $this->getDataGenerator()->create_category(); 285 $user = $this->getDataGenerator()->create_user(); 286 287 // Create a question. 288 $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); 289 $questioncategory = $questiongenerator->create_question_category(); 290 $overrides = ['name' => 'Test question', 'category' => $questioncategory->id, 291 'createdby' => $user->id, 'modifiedby' => $user->id]; 292 $question = $questiongenerator->create_question('truefalse', null, $overrides); 293 294 // Create a quiz and a questions. 295 $quiz = $this->getDataGenerator()->create_module('quiz', array('course' => $course->id)); 296 quiz_add_quiz_question($question->id, $quiz); 297 298 // Backup the course. 299 $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE, 300 backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id); 301 $backupid = $bc->get_backupid(); 302 $bc->execute_plan(); 303 $results = $bc->get_results(); 304 $file = $results['backup_destination']; 305 $fp = get_file_packer('application/vnd.moodle.backup'); 306 $filepath = $CFG->dataroot . '/temp/backup/' . $backupid; 307 $file->extract_to_pathname($fp, $filepath); 308 $bc->destroy(); 309 310 // Delete the original course and related question. 311 delete_course($course, false); 312 question_delete_question($question->id); 313 314 // Restore the course. 315 $restoredcourseid = restore_dbops::create_new_course($course->fullname, $course->shortname . '_1', $category->id); 316 $rc = new restore_controller($backupid, $restoredcourseid, backup::INTERACTIVE_NO, 317 backup::MODE_GENERAL, $USER->id, backup::TARGET_NEW_COURSE); 318 $rc->execute_precheck(); 319 $rc->execute_plan(); 320 $rc->destroy(); 321 322 // Test the question author. 323 $questions = $DB->get_records('question', ['name' => 'Test question']); 324 $this->assertCount(1, $questions); 325 $question = array_shift($questions); 326 $this->assertEquals($user->id, $question->createdby); 327 $this->assertEquals($user->id, $question->modifiedby); 328 } 329 330 /** 331 * Test that the current user is set as a question author when we are restoring the backup 332 * at the another site and the question author is not enrolled in to the course. 333 */ 334 public function test_backup_question_author_reset() { 335 global $DB, $USER, $CFG; 336 $this->resetAfterTest(); 337 $this->setAdminUser(); 338 339 // Create a course, a category and a user. 340 $course = $this->getDataGenerator()->create_course(); 341 $category = $this->getDataGenerator()->create_category(); 342 $user = $this->getDataGenerator()->create_user(); 343 344 // Create a question. 345 $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); 346 $questioncategory = $questiongenerator->create_question_category(); 347 $overrides = ['name' => 'Test question', 'category' => $questioncategory->id, 348 'createdby' => $user->id, 'modifiedby' => $user->id]; 349 $question = $questiongenerator->create_question('truefalse', null, $overrides); 350 351 // Create a quiz and a questions. 352 $quiz = $this->getDataGenerator()->create_module('quiz', array('course' => $course->id)); 353 quiz_add_quiz_question($question->id, $quiz); 354 355 // Backup the course. 356 $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE, 357 backup::INTERACTIVE_NO, backup::MODE_SAMESITE, $USER->id); 358 $backupid = $bc->get_backupid(); 359 $bc->execute_plan(); 360 $results = $bc->get_results(); 361 $file = $results['backup_destination']; 362 $fp = get_file_packer('application/vnd.moodle.backup'); 363 $filepath = $CFG->dataroot . '/temp/backup/' . $backupid; 364 $file->extract_to_pathname($fp, $filepath); 365 $bc->destroy(); 366 367 // Delete the original course and related question. 368 delete_course($course, false); 369 question_delete_question($question->id); 370 371 // Emulate restoring to a different site. 372 set_config('siteidentifier', random_string(32) . 'not the same site'); 373 374 // Restore the course. 375 $restoredcourseid = restore_dbops::create_new_course($course->fullname, $course->shortname . '_1', $category->id); 376 $rc = new restore_controller($backupid, $restoredcourseid, backup::INTERACTIVE_NO, 377 backup::MODE_SAMESITE, $USER->id, backup::TARGET_NEW_COURSE); 378 $rc->execute_precheck(); 379 $rc->execute_plan(); 380 $rc->destroy(); 381 382 // Test the question author. 383 $questions = $DB->get_records('question', ['name' => 'Test question']); 384 $this->assertCount(1, $questions); 385 $question = array_shift($questions); 386 $this->assertEquals($USER->id, $question->createdby); 387 $this->assertEquals($USER->id, $question->modifiedby); 388 } 389 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body