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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body