Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 400 and 401] [Versions 401 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 core_question;
  18  
  19  use core_question\local\bank\question_version_status;
  20  use question_bank;
  21  
  22  /**
  23   * Question version unit tests.
  24   *
  25   * @package    core_question
  26   * @copyright  2021 Catalyst IT Australia Pty Ltd
  27   * @author     Guillermo Gomez Arias <guillermogomez@catalyst-au.net>
  28   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  29   * @coversDefaultClass \question_bank
  30   */
  31  class version_test extends \advanced_testcase {
  32  
  33      /**
  34       * @var \context_module module context.
  35       */
  36      protected $context;
  37  
  38      /**
  39       * @var \stdClass course object.
  40       */
  41      protected $course;
  42  
  43      /**
  44       * @var \component_generator_base question generator.
  45       */
  46      protected $qgenerator;
  47  
  48      /**
  49       * @var \stdClass quiz object.
  50       */
  51      protected $quiz;
  52  
  53      /**
  54       * Called before every test.
  55       */
  56      protected function setUp(): void {
  57          parent::setUp();
  58          self::setAdminUser();
  59          $this->resetAfterTest();
  60  
  61          $datagenerator = $this->getDataGenerator();
  62          $this->course = $datagenerator->create_course();
  63          $this->quiz = $datagenerator->create_module('quiz', ['course' => $this->course->id]);
  64          $this->qgenerator = $datagenerator->get_plugin_generator('core_question');
  65          $this->context = \context_module::instance($this->quiz->cmid);
  66      }
  67  
  68      /**
  69       * Test if creating a question a new version and bank entry records are created.
  70       *
  71       * @covers ::load_question
  72       */
  73      public function test_make_question_create_version_and_bank_entry() {
  74          global $DB;
  75  
  76          $qcategory = $this->qgenerator->create_question_category(['contextid' => $this->context->id]);
  77          $question = $this->qgenerator->create_question('shortanswer', null, ['category' => $qcategory->id]);
  78  
  79          // Get the question object after creating a question.
  80          $questiondefinition = question_bank::load_question($question->id);
  81  
  82          // The version and bank entry in the object should be the same.
  83          $sql = "SELECT qv.id AS versionid, qv.questionbankentryid
  84                    FROM {question} q
  85                    JOIN {question_versions} qv ON qv.questionid = q.id
  86                    JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
  87                   WHERE q.id = ?";
  88          $questionversion = $DB->get_record_sql($sql, [$questiondefinition->id]);
  89          $this->assertEquals($questionversion->versionid, $questiondefinition->versionid);
  90          $this->assertEquals($questionversion->questionbankentryid, $questiondefinition->questionbankentryid);
  91  
  92          // If a question is updated, a new version should be created.
  93          $question = $this->qgenerator->update_question($question, null, ['name' => 'This is a new version']);
  94          $newquestiondefinition = question_bank::load_question($question->id);
  95          // The version should be 2.
  96          $this->assertEquals('2', $newquestiondefinition->version);
  97  
  98          // Both versions should be in same bank entry.
  99          $this->assertEquals($questiondefinition->questionbankentryid, $newquestiondefinition->questionbankentryid);
 100      }
 101  
 102      /**
 103       * Test if deleting a question the related version and bank entry records are deleted.
 104       *
 105       * @covers ::load_question
 106       * @covers ::question_delete_question
 107       */
 108      public function test_delete_question_delete_versions() {
 109          global $DB;
 110  
 111          $qcategory = $this->qgenerator->create_question_category(['contextid' => $this->context->id]);
 112          $question = $this->qgenerator->create_question('shortanswer', null, ['category' => $qcategory->id]);
 113          $questionfirstversionid = $question->id;
 114  
 115          // Create a new version and try to remove it.
 116          $question = $this->qgenerator->update_question($question, null, ['name' => 'This is a new version']);
 117  
 118          // The new version and bank entry record should exist.
 119          $sql = "SELECT q.id, qv.id AS versionid, qv.questionbankentryid
 120                    FROM {question} q
 121                    JOIN {question_versions} qv ON qv.questionid = q.id
 122                    JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
 123                   WHERE q.id = ?";
 124          $questionobject = $DB->get_records_sql($sql, [$question->id]);
 125          $this->assertCount(1, $questionobject);
 126  
 127          // Try to delete new version.
 128          question_delete_question($question->id);
 129  
 130          // The version record should not exist.
 131          $sql = "SELECT qv.*
 132                    FROM {question_versions} qv
 133                   WHERE qv.id = ?";
 134          $questionversion = $DB->get_record_sql($sql, [$questionobject[$question->id]->versionid]);
 135          $this->assertFalse($questionversion);
 136  
 137          // The bank entry record should exist because there is an older version.
 138          $sql = "SELECT qbe.*
 139                    FROM {question_bank_entries} qbe
 140                   WHERE qbe.id = ?";
 141          $questionbankentry = $DB->get_records_sql($sql, [$questionobject[$question->id]->questionbankentryid]);
 142          $this->assertCount(1, $questionbankentry);
 143  
 144          // Now remove the first version.
 145          question_delete_question($questionfirstversionid);
 146          $sql = "SELECT qbe.*
 147                    FROM {question_bank_entries} qbe
 148                   WHERE qbe.id = ?";
 149          $questionbankentry = $DB->get_record_sql($sql, [$questionobject[$question->id]->questionbankentryid]);
 150          // The bank entry record should not exist.
 151          $this->assertFalse($questionbankentry);
 152      }
 153  
 154      /**
 155       * Test if deleting a question will not break a quiz.
 156       *
 157       * @covers ::load_question
 158       * @covers ::quiz_add_quiz_question
 159       * @covers ::question_delete_question
 160       */
 161      public function test_delete_question_in_use() {
 162          global $DB;
 163  
 164          $qcategory = $this->qgenerator->create_question_category(['contextid' => $this->context->id]);
 165          $question = $this->qgenerator->create_question('shortanswer', null, ['category' => $qcategory->id]);
 166          $questionfirstversionid = $question->id;
 167  
 168          // Create a new version and try to remove it after adding it to a quiz.
 169          $question = $this->qgenerator->update_question($question, null, ['name' => 'This is a new version']);
 170  
 171          // Add it to the quiz.
 172          quiz_add_quiz_question($question->id, $this->quiz);
 173  
 174          // Try to delete new version.
 175          question_delete_question($question->id);
 176          // Try to delete old version.
 177          question_delete_question($questionfirstversionid);
 178  
 179          // The used question version should exist even after trying to remove it, but now hidden.
 180          $questionversion2 = question_bank::load_question($question->id);
 181          $this->assertEquals($question->id, $questionversion2->id);
 182          $this->assertEquals(question_version_status::QUESTION_STATUS_HIDDEN, $questionversion2->status);
 183  
 184          // The unused version should be completely gone.
 185          $this->assertFalse($DB->record_exists('question', ['id' => $questionfirstversionid]));
 186      }
 187  
 188      /**
 189       * Test if moving a category will not break a quiz.
 190       *
 191       * @covers ::load_question
 192       * @covers ::quiz_add_quiz_question
 193       */
 194      public function test_move_category_with_questions() {
 195          global $DB;
 196  
 197          $qcategory = $this->qgenerator->create_question_category(['contextid' => $this->context->id]);
 198          $qcategorychild = $this->qgenerator->create_question_category(['contextid' => $this->context->id,
 199              'parent' => $qcategory->id]);
 200          $systemcontext = \context_system::instance();
 201          $qcategorysys = $this->qgenerator->create_question_category(['contextid' => $systemcontext->id]);
 202          $question = $this->qgenerator->create_question('shortanswer', null, ['category' => $qcategorychild->id]);
 203          $questiondefinition = question_bank::load_question($question->id);
 204  
 205          // Add it to the quiz.
 206          quiz_add_quiz_question($question->id, $this->quiz);
 207  
 208          // Move the category to system context.
 209          $contexts = new \core_question\local\bank\question_edit_contexts($systemcontext);
 210          $qcobject = new \qbank_managecategories\question_category_object(null,
 211              new \moodle_url('/question/bank/managecategories/category.php', ['courseid' => SITEID]),
 212              $contexts->having_one_edit_tab_cap('categories'), 0, null, 0,
 213              $contexts->having_cap('moodle/question:add'));
 214          $qcobject->move_questions_and_delete_category($qcategorychild->id, $qcategorysys->id);
 215  
 216          // The bank entry record should point to the new category in order to not break quizzes.
 217          $sql = "SELECT qbe.questioncategoryid
 218                    FROM {question_bank_entries} qbe
 219                   WHERE qbe.id = ?";
 220          $questionbankentry = $DB->get_record_sql($sql, [$questiondefinition->questionbankentryid]);
 221          $this->assertEquals($qcategorysys->id, $questionbankentry->questioncategoryid);
 222      }
 223  
 224      /**
 225       * Test that all versions will have the same bank entry idnumber value.
 226       *
 227       * @covers ::load_question
 228       */
 229      public function test_id_number_in_bank_entry() {
 230          global $DB;
 231  
 232          $qcategory = $this->qgenerator->create_question_category(['contextid' => $this->context->id]);
 233          $question = $this->qgenerator->create_question('shortanswer', null,
 234              [
 235                  'category' => $qcategory->id,
 236                  'idnumber' => 'id1'
 237              ]);
 238          $questionid1 = $question->id;
 239  
 240          // Create a new version and try to remove it after adding it to a quiz.
 241          $question = $this->qgenerator->update_question($question, null, ['idnumber' => 'id2']);
 242          $questionid2 = $question->id;
 243          // Change the id number and get the question object.
 244          $question = $this->qgenerator->update_question($question, null, ['idnumber' => 'id3']);
 245          $questionid3 = $question->id;
 246  
 247          // The new version and bank entry record should exist.
 248          $questiondefinition = question_bank::load_question($question->id);
 249          $sql = "SELECT q.id AS questionid, qv.id AS versionid, qbe.id AS questionbankentryid, qbe.idnumber
 250                    FROM {question_bank_entries} qbe
 251                    JOIN {question_versions} qv ON qv.questionbankentryid = qbe.id
 252                    JOIN {question} q ON q.id = qv.questionid
 253                   WHERE qbe.id = ?";
 254          $questionbankentry = $DB->get_records_sql($sql, [$questiondefinition->questionbankentryid]);
 255  
 256          // We should have 3 versions and 1 question bank entry with the same idnumber.
 257          $this->assertCount(3, $questionbankentry);
 258          $this->assertEquals($questionbankentry[$questionid1]->idnumber, 'id3');
 259          $this->assertEquals($questionbankentry[$questionid2]->idnumber, 'id3');
 260          $this->assertEquals($questionbankentry[$questionid3]->idnumber, 'id3');
 261      }
 262  
 263      /**
 264       * Test that all the versions are available from the method.
 265       *
 266       * @covers ::get_all_versions_of_question
 267       */
 268      public function test_get_all_versions_of_question() {
 269          $qcategory = $this->qgenerator->create_question_category(['contextid' => $this->context->id]);
 270          $question = $this->qgenerator->create_question('shortanswer', null,
 271              [
 272                  'category' => $qcategory->id,
 273                  'idnumber' => 'id1'
 274              ]);
 275          $questionid1 = $question->id;
 276  
 277          // Create a new version.
 278          $question = $this->qgenerator->update_question($question, null, ['idnumber' => 'id2']);
 279          $questionid2 = $question->id;
 280          // Change the id number and get the question object.
 281          $question = $this->qgenerator->update_question($question, null, ['idnumber' => 'id3']);
 282          $questionid3 = $question->id;
 283  
 284          $questiondefinition = question_bank::get_all_versions_of_question($question->id);
 285  
 286          // Test the versions are available.
 287          $this->assertEquals(array_slice($questiondefinition, 0, 1)[0]->questionid, $questionid3);
 288          $this->assertEquals(array_slice($questiondefinition, 1, 1)[0]->questionid, $questionid2);
 289          $this->assertEquals(array_slice($questiondefinition, 2, 1)[0]->questionid, $questionid1);
 290      }
 291  
 292      /**
 293       * Test that all the versions of questions are available from the method.
 294       *
 295       * @covers ::get_all_versions_of_questions
 296       */
 297      public function test_get_all_versions_of_questions() {
 298          global $DB;
 299  
 300          $questionversions = [];
 301          $qcategory = $this->qgenerator->create_question_category(['contextid' => $this->context->id]);
 302          $question = $this->qgenerator->create_question('shortanswer', null,
 303              [
 304                  'category' => $qcategory->id,
 305                  'idnumber' => 'id1'
 306              ]);
 307          $questionversions[1] = $question->id;
 308  
 309          // Create a new version.
 310          $question = $this->qgenerator->update_question($question, null, ['idnumber' => 'id2']);
 311          $questionversions[2] = $question->id;
 312          // Change the id number and get the question object.
 313          $question = $this->qgenerator->update_question($question, null, ['idnumber' => 'id3']);
 314          $questionversions[3] = $question->id;
 315  
 316          $questionbankentryid = $DB->get_record('question_versions', ['questionid' => $question->id], 'questionbankentryid');
 317  
 318          $questionversionsofquestions = question_bank::get_all_versions_of_questions([$question->id]);
 319          $questionbankentryids = array_keys($questionversionsofquestions)[0];
 320          $this->assertEquals($questionbankentryid->questionbankentryid, $questionbankentryids);
 321          $this->assertEquals($questionversions, $questionversionsofquestions[$questionbankentryids]);
 322      }
 323  }