Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 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 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  }