Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403] [Versions 39 and 310]

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