MDL-73975 course: Remove course_search_form template
[moodle.git] / question / tests / version_test.php
blob516e7290a19a893e69e9f88da46754194edad262
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.
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/>.
17 namespace core_question;
19 use core_question\local\bank\question_version_status;
20 use core_question\output\question_version_info;
21 use question_bank;
23 /**
24 * Question version unit tests.
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
32 class version_test extends \advanced_testcase {
34 /**
35 * @var \context_module module context.
37 protected $context;
39 /**
40 * @var \stdClass course object.
42 protected $course;
44 /**
45 * @var \component_generator_base question generator.
47 protected $qgenerator;
49 /**
50 * @var \stdClass quiz object.
52 protected $quiz;
54 /**
55 * Called before every test.
57 protected function setUp(): void {
58 parent::setUp();
59 self::setAdminUser();
60 $this->resetAfterTest();
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);
69 protected function tearDown(): void {
70 question_version_info::$pendingdefinitions = [];
71 parent::tearDown();
74 /**
75 * Test if creating a question a new version and bank entry records are created.
77 * @covers ::load_question
79 public function test_make_question_create_version_and_bank_entry() {
80 global $DB;
82 $qcategory = $this->qgenerator->create_question_category(['contextid' => $this->context->id]);
83 $question = $this->qgenerator->create_question('shortanswer', null, ['category' => $qcategory->id]);
85 // Get the question object after creating a question.
86 $questiondefinition = question_bank::load_question($question->id);
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);
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);
104 // Both versions should be in same bank entry.
105 $this->assertEquals($questiondefinition->questionbankentryid, $newquestiondefinition->questionbankentryid);
109 * Test if deleting a question the related version and bank entry records are deleted.
111 * @covers ::load_question
112 * @covers ::question_delete_question
114 public function test_delete_question_delete_versions() {
115 global $DB;
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;
121 // Create a new version and try to remove it.
122 $question = $this->qgenerator->update_question($question, null, ['name' => 'This is a new version']);
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);
133 // Try to delete new version.
134 question_delete_question($question->id);
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);
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);
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);
161 * Test if deleting a question will not break a quiz.
163 * @covers ::load_question
164 * @covers ::quiz_add_quiz_question
165 * @covers ::question_delete_question
167 public function test_delete_question_in_use() {
168 global $DB;
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;
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']);
177 // Add it to the quiz.
178 quiz_add_quiz_question($question->id, $this->quiz);
180 // Try to delete new version.
181 question_delete_question($question->id);
182 // Try to delete old version.
183 question_delete_question($questionfirstversionid);
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);
190 // The unused version should be completely gone.
191 $this->assertFalse($DB->record_exists('question', ['id' => $questionfirstversionid]));
195 * Test if moving a category will not break a quiz.
197 * @covers ::load_question
198 * @covers ::quiz_add_quiz_question
200 public function test_move_category_with_questions() {
201 global $DB;
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);
211 // Add it to the quiz.
212 quiz_add_quiz_question($question->id, $this->quiz);
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);
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);
231 * Test that all versions will have the same bank entry idnumber value.
233 * @covers ::load_question
235 public function test_id_number_in_bank_entry() {
236 global $DB;
238 $qcategory = $this->qgenerator->create_question_category(['contextid' => $this->context->id]);
239 $question = $this->qgenerator->create_question('shortanswer', null,
241 'category' => $qcategory->id,
242 'idnumber' => 'id1'
244 $questionid1 = $question->id;
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;
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]);
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');
270 * Test that all the versions are available from the method.
272 * @covers ::get_all_versions_of_question
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,
278 'category' => $qcategory->id,
279 'idnumber' => 'id1'
281 $questionid1 = $question->id;
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;
290 $questiondefinition = question_bank::get_all_versions_of_question($question->id);
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);
299 * Test that all the versions of questions are available from the method.
301 * @covers ::get_all_versions_of_questions
303 public function test_get_all_versions_of_questions() {
304 global $DB;
306 $questionversions = [];
307 $qcategory = $this->qgenerator->create_question_category(['contextid' => $this->context->id]);
308 $question = $this->qgenerator->create_question('shortanswer', null,
310 'category' => $qcategory->id,
311 'idnumber' => 'id1'
313 $questionversions[1] = $question->id;
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;
322 $questionbankentryid = $DB->get_record('question_versions', ['questionid' => $question->id], 'questionbankentryid');
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]);
331 * Test population of latestversion field in question_definition objects
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.
337 * @covers \core_question\output\question_version_info::populate_latest_versions()
338 * @return void
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']);
346 $latestversioninspector = new \ReflectionProperty('question_definition', 'latestversion');
347 $this->assertEmpty(question_version_info::$pendingdefinitions);
349 $questiondef1 = question_bank::load_question($question1->id);
350 $questiondef2 = question_bank::load_question($question2->id);
351 $questiondef3 = question_bank::load_question($question3->id);
353 $this->assertContains($questiondef1, question_version_info::$pendingdefinitions);
354 $this->assertContains($questiondef2, question_version_info::$pendingdefinitions);
355 $this->assertContains($questiondef3, question_version_info::$pendingdefinitions);
356 $this->assertNull($latestversioninspector->getValue($questiondef1));
357 $this->assertNull($latestversioninspector->getValue($questiondef2));
358 $this->assertNull($latestversioninspector->getValue($questiondef3));
360 // Read latestversion from one definition. This should populate the field in all pending definitions.
361 $latestversion1 = $questiondef1->latestversion;
363 $this->assertEmpty(question_version_info::$pendingdefinitions);
364 $this->assertNotNull($latestversioninspector->getValue($questiondef1));
365 $this->assertNotNull($latestversioninspector->getValue($questiondef2));
366 $this->assertNotNull($latestversioninspector->getValue($questiondef3));
367 $this->assertEquals($latestversion1, $latestversioninspector->getValue($questiondef1));
368 $this->assertEquals($questiondef1->version, $questiondef1->latestversion);
369 $this->assertNotEquals($questiondef2->version, $questiondef2->latestversion);
370 $this->assertEquals($questiondef3->version, $questiondef3->latestversion);