Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

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