Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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

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