1441 |
ariadna |
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 mod_qbank\task;
|
|
|
18 |
|
|
|
19 |
use context;
|
|
|
20 |
use context_course;
|
|
|
21 |
use context_coursecat;
|
|
|
22 |
use context_module;
|
|
|
23 |
use context_system;
|
|
|
24 |
use core\task\manager;
|
|
|
25 |
use core_question\local\bank\random_question_loader;
|
|
|
26 |
use core_question\local\bank\question_bank_helper;
|
|
|
27 |
use mod_quiz\quiz_settings;
|
|
|
28 |
use stdClass;
|
|
|
29 |
use core_question\local\bank\question_version_status;
|
|
|
30 |
|
|
|
31 |
/**
|
|
|
32 |
* Before testing, we firstly need to create some data to emulate what sites can have pre-upgrade.
|
|
|
33 |
* Namely, we are adding question categories and questions to deprecated contexts i.e. anything not CONTEXT_MODULE,
|
|
|
34 |
* and to quiz local banks too as we need to test these don't get touched.
|
|
|
35 |
* It also adds questions to some categories that are not used by quizzes anywhere.
|
|
|
36 |
*
|
|
|
37 |
* The tests cover a few areas.
|
|
|
38 |
* 1: We validate the data setup is correct before we run the installation script testing.
|
|
|
39 |
* 2: The installation test validates that any question categories not in CONTEXT_MODULE get transferred to relevant mod_qbank
|
|
|
40 |
* instances including their questions. It also validates that any stale questions that are not in use by quizzes are removed
|
|
|
41 |
* along with empty categories.
|
|
|
42 |
*
|
|
|
43 |
* @package mod_qbank
|
|
|
44 |
* @copyright 2024 onwards Catalyst IT EU {@link https://catalyst-eu.net}
|
|
|
45 |
* @author Simon Adams <simon.adams@catalyst-eu.net>
|
|
|
46 |
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
|
47 |
* @covers \mod_qbank\task\transfer_question_categories
|
|
|
48 |
*/
|
|
|
49 |
final class transfer_question_categories_test extends \advanced_testcase {
|
|
|
50 |
|
|
|
51 |
/** @var \core\context\coursecat test course category context */
|
|
|
52 |
private \core\context\coursecat $coursecatcontext;
|
|
|
53 |
|
|
|
54 |
/** @var \core\context\course test course context */
|
|
|
55 |
private \core\context\course $coursecontext;
|
|
|
56 |
|
|
|
57 |
/** @var \core\context\course test stale course context*/
|
|
|
58 |
private \core\context\course $stalecoursecontext;
|
|
|
59 |
|
|
|
60 |
/** @var \core\context\module test quiz mod context */
|
|
|
61 |
private \core\context\module $quizcontext;
|
|
|
62 |
|
|
|
63 |
/** @var \core\context\course Course context with used and unused questions. */
|
|
|
64 |
private \core\context\course $usedunusedcontext;
|
|
|
65 |
|
|
|
66 |
/** @var stdClass[] test stale questions */
|
|
|
67 |
private array $stalequestions;
|
|
|
68 |
|
|
|
69 |
/**
|
|
|
70 |
* Get question data from question category ids provided in the argument.
|
|
|
71 |
*
|
|
|
72 |
* @param array $categoryids
|
|
|
73 |
* @return array
|
|
|
74 |
*/
|
|
|
75 |
protected function get_question_data(array $categoryids): array {
|
|
|
76 |
global $DB;
|
|
|
77 |
|
|
|
78 |
[$insql, $inparams] = $DB->get_in_or_equal($categoryids);
|
|
|
79 |
|
|
|
80 |
$sql = "SELECT q.id, qbe.questioncategoryid AS categoryid, qv.status
|
|
|
81 |
FROM {question} q
|
|
|
82 |
JOIN {question_versions} qv ON qv.questionid = q.id
|
|
|
83 |
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
|
|
84 |
WHERE qbe.questioncategoryid {$insql}";
|
|
|
85 |
|
|
|
86 |
return $DB->get_records_sql($sql, $inparams);
|
|
|
87 |
}
|
|
|
88 |
|
|
|
89 |
/**
|
|
|
90 |
* This is hacky, but we can't use the API to create these as non module contexts are deprecated for holding question
|
|
|
91 |
* categories.
|
|
|
92 |
*
|
|
|
93 |
* @param string $name of the new category
|
|
|
94 |
* @param int $contextid of the module holding the category
|
|
|
95 |
* @param int $parentid of the new category
|
|
|
96 |
* @return stdClass category object
|
|
|
97 |
*/
|
|
|
98 |
protected function create_question_category(string $name, int $contextid, int $parentid = 0): stdClass {
|
|
|
99 |
|
|
|
100 |
global $DB;
|
|
|
101 |
|
|
|
102 |
if (!$parentid) {
|
|
|
103 |
if (!$parent = $DB->get_record('question_categories', ['contextid' => $contextid, 'parent' => 0, 'name' => 'top'])) {
|
|
|
104 |
$parent = new stdClass();
|
|
|
105 |
$parent->name = 'top';
|
|
|
106 |
$parent->info = '';
|
|
|
107 |
$parent->contextid = $contextid;
|
|
|
108 |
$parent->parent = 0;
|
|
|
109 |
$parent->sortorder = 0;
|
|
|
110 |
$parent->stamp = make_unique_id_code();
|
|
|
111 |
$parent->id = $DB->insert_record('question_categories', $parent);
|
|
|
112 |
}
|
|
|
113 |
$parentid = $parent->id;
|
|
|
114 |
}
|
|
|
115 |
|
|
|
116 |
$record = (object) [
|
|
|
117 |
'name' => $name,
|
|
|
118 |
'parent' => $parentid,
|
|
|
119 |
'contextid' => $contextid,
|
|
|
120 |
'info' => '',
|
|
|
121 |
'infoformat' => FORMAT_HTML,
|
|
|
122 |
'stamp' => make_unique_id_code(),
|
|
|
123 |
'sortorder' => 999,
|
|
|
124 |
'idnumber' => null,
|
|
|
125 |
];
|
|
|
126 |
|
|
|
127 |
$record->id = $DB->insert_record('question_categories', $record);
|
|
|
128 |
return $record;
|
|
|
129 |
}
|
|
|
130 |
|
|
|
131 |
/**
|
|
|
132 |
* Sets up the installation test with data.
|
|
|
133 |
*
|
|
|
134 |
* @return void
|
|
|
135 |
*/
|
|
|
136 |
protected function setup_pre_install_data(): void {
|
|
|
137 |
global $DB;
|
|
|
138 |
self::setAdminUser();
|
|
|
139 |
$questiongenerator = self::getDataGenerator()->get_plugin_generator('core_question');
|
|
|
140 |
$quizgenerator = self::getDataGenerator()->get_plugin_generator('mod_quiz');
|
|
|
141 |
|
|
|
142 |
// Setup 2 categories at site level context, with a question in each.
|
|
|
143 |
$sitecontext = context_system::instance();
|
|
|
144 |
$site = get_site();
|
|
|
145 |
|
|
|
146 |
$siteparentcat = $this->create_question_category('Site Parent Cat', $sitecontext->id);
|
|
|
147 |
|
|
|
148 |
$sitechildcat = $this->create_question_category('Site Child Cat', $sitecontext->id, $siteparentcat->id);
|
|
|
149 |
|
|
|
150 |
$question1 = $questiongenerator->create_question(
|
|
|
151 |
'shortanswer',
|
|
|
152 |
null,
|
|
|
153 |
['category' => $siteparentcat->id, 'status' => question_version_status::QUESTION_STATUS_READY]
|
|
|
154 |
);
|
|
|
155 |
$question2 = $questiongenerator->create_question(
|
|
|
156 |
'shortanswer',
|
|
|
157 |
null,
|
|
|
158 |
['category' => $sitechildcat->id, 'status' => question_version_status::QUESTION_STATUS_READY]
|
|
|
159 |
);
|
|
|
160 |
|
|
|
161 |
// Add a quiz to the site course and put those questions into it.
|
|
|
162 |
$quiz = $quizgenerator->create_instance(['course' => $site->id, 'grade' => 100.0, 'sumgrades' => 2, 'layout' => '1,0']);
|
|
|
163 |
quiz_add_quiz_question($question1->id, $quiz, 1);
|
|
|
164 |
quiz_add_quiz_question($question2->id, $quiz, 1);
|
|
|
165 |
|
|
|
166 |
// Create a course with a quiz containing a random question from the system context.
|
|
|
167 |
$randomcourse = self::getDataGenerator()->create_course(['shortname' => 'Random']);
|
|
|
168 |
$randomquiz = $quizgenerator->create_instance(
|
|
|
169 |
[
|
|
|
170 |
'course' => $randomcourse->id,
|
|
|
171 |
'grade' => 100.0,
|
|
|
172 |
'sumgrades' => 2,
|
|
|
173 |
'layout' => '1,0',
|
|
|
174 |
],
|
|
|
175 |
);
|
|
|
176 |
$randomquizsettings = quiz_settings::create($randomquiz->id);
|
|
|
177 |
$structure = $randomquizsettings->get_structure();
|
|
|
178 |
$topcategory = $DB->get_record('question_categories', ['contextid' => $sitecontext->id, 'parent' => 0]);
|
|
|
179 |
$filtercondition = [
|
|
|
180 |
'filter' => [
|
|
|
181 |
'category' => [
|
|
|
182 |
'jointype' => \core_question\local\bank\condition::JOINTYPE_DEFAULT,
|
|
|
183 |
'values' => [$topcategory->id],
|
|
|
184 |
'filteroptions' => ['includesubcategories' => true],
|
|
|
185 |
],
|
|
|
186 |
],
|
|
|
187 |
];
|
|
|
188 |
$structure->add_random_questions(1, 1, $filtercondition);
|
|
|
189 |
|
|
|
190 |
// Create a course category and then a question category attached to that context.
|
|
|
191 |
$coursecategory = self::getDataGenerator()->create_category();
|
|
|
192 |
$this->coursecatcontext = context_coursecat::instance($coursecategory->id);
|
|
|
193 |
$coursecatcat = $this->create_question_category('Course Cat Parent Cat', $this->coursecatcontext->id);
|
|
|
194 |
|
|
|
195 |
// Add a question to the category just made.
|
|
|
196 |
$question3 = $questiongenerator->create_question('essay', 'files', ['category' => $coursecatcat->id]);
|
|
|
197 |
|
|
|
198 |
// Add a quiz to the course category and put those questions into it.
|
|
|
199 |
$course = self::getDataGenerator()->create_course(['category' => $coursecategory->id]);
|
|
|
200 |
$quiz = $quizgenerator->create_instance(['course' => $course->id, 'grade' => 100.0, 'sumgrades' => 2, 'layout' => '1,0']);
|
|
|
201 |
quiz_add_quiz_question($question3->id, $quiz, 1);
|
|
|
202 |
|
|
|
203 |
// Create an additional question with a missing type, to catch edge cases.
|
|
|
204 |
$question4 = $questiongenerator->create_question('missingtype', 'invalid', ['category' => $coursecatcat->id]);
|
|
|
205 |
$DB->set_field('question', 'qtype', 'invalid', ['id' => $question4->id]);
|
|
|
206 |
|
|
|
207 |
// Create 2 nested categories with questions in them at course context level.
|
|
|
208 |
$course = self::getDataGenerator()->create_course();
|
|
|
209 |
$this->coursecontext = context_course::instance($course->id);
|
|
|
210 |
$coursegrandparentcat = $this->create_question_category('Course Grandparent Cat', $this->coursecontext->id);
|
|
|
211 |
$courseparentcat1 = $this->create_question_category(
|
|
|
212 |
'Course Parent Cat',
|
|
|
213 |
$this->coursecontext->id,
|
|
|
214 |
$coursegrandparentcat->id,
|
|
|
215 |
);
|
|
|
216 |
$coursechildcat1 = $this->create_question_category(
|
|
|
217 |
'Course Child Cat',
|
|
|
218 |
$this->coursecontext->id,
|
|
|
219 |
$courseparentcat1->id,
|
|
|
220 |
);
|
|
|
221 |
|
|
|
222 |
$question4 = $questiongenerator->create_question('shortanswer', null, ['category' => $courseparentcat1->id]);
|
|
|
223 |
$question5 = $questiongenerator->create_question('shortanswer', null, ['category' => $coursechildcat1->id]);
|
|
|
224 |
|
|
|
225 |
// Make the questions 'in use'.
|
|
|
226 |
$quiz = $quizgenerator->create_instance(['course' => $course->id, 'grade' => 100.0, 'sumgrades' => 2, 'layout' => '1,0']);
|
|
|
227 |
quiz_add_quiz_question($question4->id, $quiz, 1);
|
|
|
228 |
quiz_add_quiz_question($question5->id, $quiz, 1);
|
|
|
229 |
|
|
|
230 |
// Include a stale question, which should not be migrated with the others.
|
|
|
231 |
$question6 = $questiongenerator->create_question('shortanswer', null, ['category' => $coursechildcat1->id]);
|
|
|
232 |
$DB->set_field(
|
|
|
233 |
'question_versions',
|
|
|
234 |
'status',
|
|
|
235 |
question_version_status::QUESTION_STATUS_HIDDEN,
|
|
|
236 |
['questionid' => $question6->id],
|
|
|
237 |
);
|
|
|
238 |
|
|
|
239 |
// Create some nested categories with no questions in use.
|
|
|
240 |
$course = self::getDataGenerator()->create_course();
|
|
|
241 |
$context = context_course::instance($course->id);
|
|
|
242 |
$courseparentcat1 = $this->create_question_category('Stale Course Parent Cat1', $context->id);
|
|
|
243 |
$coursechildcat1 = $this->create_question_category('Stale Course Child Cat1', $context->id, $courseparentcat1->id);
|
|
|
244 |
$courseparentcat2 = $this->create_question_category('Stale Course Parent Cat2', $context->id);
|
|
|
245 |
$coursechildcat2 = $this->create_question_category('Stale Course Child Cat2', $context->id, $courseparentcat2->id);
|
|
|
246 |
$coursegrandchildcat1 = $this->create_question_category('Stale Course Grandchild Cat1', $context->id, $coursechildcat2->id);
|
|
|
247 |
$this->stalecoursecontext = context_course::instance($course->id);
|
|
|
248 |
|
|
|
249 |
// Make all the questions hidden.
|
|
|
250 |
$this->stalequestions[] = $questiongenerator->create_question('shortanswer',
|
|
|
251 |
null,
|
|
|
252 |
['category' => $courseparentcat1->id, 'status' => question_version_status::QUESTION_STATUS_HIDDEN]
|
|
|
253 |
);
|
|
|
254 |
$this->stalequestions[] = $questiongenerator->create_question('shortanswer',
|
|
|
255 |
null,
|
|
|
256 |
['category' => $coursechildcat1->id, 'status' => question_version_status::QUESTION_STATUS_HIDDEN]
|
|
|
257 |
);
|
|
|
258 |
$this->stalequestions[] = $questiongenerator->create_question('shortanswer',
|
|
|
259 |
null,
|
|
|
260 |
['category' => $courseparentcat2->id, 'status' => question_version_status::QUESTION_STATUS_HIDDEN]
|
|
|
261 |
);
|
|
|
262 |
$this->stalequestions[] = $questiongenerator->create_question('shortanswer',
|
|
|
263 |
null,
|
|
|
264 |
['category' => $coursechildcat2->id, 'status' => question_version_status::QUESTION_STATUS_HIDDEN]
|
|
|
265 |
);
|
|
|
266 |
$this->stalequestions[] = $questiongenerator->create_question('shortanswer',
|
|
|
267 |
null,
|
|
|
268 |
['category' => $coursegrandchildcat1->id, 'status' => question_version_status::QUESTION_STATUS_HIDDEN]
|
|
|
269 |
);
|
|
|
270 |
|
|
|
271 |
foreach ($this->stalequestions as $question) {
|
|
|
272 |
$DB->set_field('question_versions',
|
|
|
273 |
'status',
|
|
|
274 |
question_version_status::QUESTION_STATUS_HIDDEN,
|
|
|
275 |
['questionid' => $question->id]
|
|
|
276 |
);
|
|
|
277 |
}
|
|
|
278 |
|
|
|
279 |
// Create additional versions of a stale question, all hidden.
|
|
|
280 |
$staleversionquestion = reset($this->stalequestions);
|
|
|
281 |
$questiongenerator->update_question($staleversionquestion, overrides: (array) $staleversionquestion);
|
|
|
282 |
$questiongenerator->update_question($staleversionquestion, overrides: (array) $staleversionquestion);
|
|
|
283 |
|
|
|
284 |
// Set up a quiz with some categories and questions attached to it.
|
|
|
285 |
$course = self::getDataGenerator()->create_course();
|
|
|
286 |
$quiz = $quizgenerator->create_instance(['course' => $course->id, 'grade' => 100.0, 'sumgrades' => 2, 'layout' => '1,0']);
|
|
|
287 |
$this->quizcontext = context_module::instance($quiz->cmid);
|
|
|
288 |
$quizparentcat1 = $this->create_question_category('Quiz Mod Parent Cat1', $this->quizcontext->id);
|
|
|
289 |
$quizchildcat1 = $this->create_question_category('Quiz Mod Child Cat1', $this->quizcontext->id, $quizparentcat1->id);
|
|
|
290 |
$question1 = $questiongenerator->create_question('shortanswer', null, ['category' => $quizparentcat1->id]);
|
|
|
291 |
$question2 = $questiongenerator->create_question('shortanswer', null, ['category' => $quizchildcat1->id]);
|
|
|
292 |
quiz_add_quiz_question($question1->id, $quiz, 1);
|
|
|
293 |
quiz_add_quiz_question($question2->id, $quiz, 1);
|
|
|
294 |
|
|
|
295 |
// Set up a course with three categories
|
|
|
296 |
// - One contains questions including 1 that is used in a quiz.
|
|
|
297 |
// - One contains questions that are not used anywhere, but are in "ready" state.
|
|
|
298 |
// - One contains no questions.
|
|
|
299 |
$course = self::getDataGenerator()->create_course(['shortname' => 'Used-Unused-Empty']);
|
|
|
300 |
$this->usedunusedcontext = context_course::instance($course->id);
|
|
|
301 |
$usedcategory = $this->create_question_category(name: 'Used Question Cat', contextid: $this->usedunusedcontext->id);
|
|
|
302 |
$unusedcategory = $this->create_question_category('Unused Question Cat', $this->usedunusedcontext->id);
|
|
|
303 |
$emptycategory = $this->create_question_category('Empty Cat', $this->usedunusedcontext->id);
|
|
|
304 |
$question1 = $questiongenerator->create_question('shortanswer', null, ['category' => $usedcategory->id]);
|
|
|
305 |
$question2 = $questiongenerator->create_question('shortanswer', null, ['category' => $usedcategory->id]);
|
|
|
306 |
$question3 = $questiongenerator->create_question('shortanswer', null, ['category' => $unusedcategory->id]);
|
|
|
307 |
$question4 = $questiongenerator->create_question('shortanswer', null, ['category' => $unusedcategory->id]);
|
|
|
308 |
$quiz = $quizgenerator->create_instance(['course' => $course->id, 'grade' => 100.0, 'sumgrades' => 2, 'layout' => '1,0']);
|
|
|
309 |
quiz_add_quiz_question($question1->id, $quiz, 1);
|
|
|
310 |
|
|
|
311 |
// The quiz also contains a random question from the used category.
|
|
|
312 |
$quizsettings = quiz_settings::create($quiz->id);
|
|
|
313 |
$structure = $quizsettings->get_structure();
|
|
|
314 |
$filtercondition = [
|
|
|
315 |
'filter' => [
|
|
|
316 |
'category' => [
|
|
|
317 |
'jointype' => \core_question\local\bank\condition::JOINTYPE_DEFAULT,
|
|
|
318 |
'values' => [$usedcategory->id],
|
|
|
319 |
'filteroptions' => ['includesubcategories' => false],
|
|
|
320 |
],
|
|
|
321 |
],
|
|
|
322 |
];
|
|
|
323 |
$structure->add_random_questions(1, 1, $filtercondition);
|
|
|
324 |
}
|
|
|
325 |
|
|
|
326 |
/**
|
|
|
327 |
* Asserts that the pre-installation setup is correct.
|
|
|
328 |
*
|
|
|
329 |
* @return void
|
|
|
330 |
*/
|
|
|
331 |
public function test_setup_pre_install_data(): void {
|
|
|
332 |
global $DB;
|
|
|
333 |
$this->resetAfterTest();
|
|
|
334 |
$this->setup_pre_install_data();
|
|
|
335 |
|
|
|
336 |
$sitecontext = context_system::instance();
|
|
|
337 |
$allsitecats = $DB->get_records('question_categories', ['contextid' => $sitecontext->id], 'id ASC');
|
|
|
338 |
|
|
|
339 |
// Make sure we have 2 site level question categories below 'top' and that the child is below the parent.
|
|
|
340 |
$this->assertCount(3, $allsitecats);
|
|
|
341 |
$parentcat = next($allsitecats);
|
|
|
342 |
$childcat = end($allsitecats);
|
|
|
343 |
$this->assertEquals($parentcat->id, $childcat->parent);
|
|
|
344 |
|
|
|
345 |
// Make sure we have 1 question per the above site level question categories.
|
|
|
346 |
$questions = $this->get_question_data(array_map(static fn($cat) => $cat->id, $allsitecats));
|
|
|
347 |
usort($questions, static fn($a, $b) => $a->categoryid <=> $b->categoryid);
|
|
|
348 |
$this->assertCount(2, $questions);
|
|
|
349 |
$parentcatq = reset($questions);
|
|
|
350 |
$childcatq = end($questions);
|
|
|
351 |
$this->assertEquals($parentcat->id, $parentcatq->categoryid);
|
|
|
352 |
$this->assertEquals($childcat->id, $childcatq->categoryid);
|
|
|
353 |
|
|
|
354 |
// Make sure the "Random" course has 1 quiz with 1 random question that returns the questions from the system top category.
|
|
|
355 |
$randomcourse = $DB->get_record('course', ['shortname' => 'Random']);
|
|
|
356 |
$coursemods = get_course_mods($randomcourse->id);
|
|
|
357 |
$randomquiz = reset($coursemods);
|
|
|
358 |
$randomquizsettings = quiz_settings::create($randomquiz->instance);
|
|
|
359 |
$structure = $randomquizsettings->get_structure();
|
|
|
360 |
$randomquestionslot = $structure->get_question_in_slot(1);
|
|
|
361 |
$this->assertEquals($randomquestionslot->contextid, $sitecontext->id);
|
|
|
362 |
$loader = new random_question_loader(new \qubaid_list([]));
|
|
|
363 |
$randomquestions = $loader->get_filtered_questions($randomquestionslot->filtercondition['filter']);
|
|
|
364 |
$this->assertCount(2, $randomquestions);
|
|
|
365 |
$randomq1 = reset($randomquestions);
|
|
|
366 |
$randomq2 = end($randomquestions);
|
|
|
367 |
$this->assertEquals($parentcatq->id, $randomq1->id);
|
|
|
368 |
$this->assertEquals($parentcat->id, $randomq1->category);
|
|
|
369 |
$this->assertEquals($childcatq->id, $randomq2->id);
|
|
|
370 |
$this->assertEquals($childcat->id, $randomq2->category);
|
|
|
371 |
|
|
|
372 |
// Make sure that the course category has a question category below 'top'.
|
|
|
373 |
$allcoursecatcats = $DB->get_records('question_categories', ['contextid' => $this->coursecatcontext->id], 'id ASC');
|
|
|
374 |
$this->assertCount(2, $allcoursecatcats);
|
|
|
375 |
$topcat = reset($allcoursecatcats);
|
|
|
376 |
$parentcat = end($allcoursecatcats);
|
|
|
377 |
$this->assertEquals($topcat->id, $parentcat->parent);
|
|
|
378 |
|
|
|
379 |
// Make sure we have 2 questions in the above course category level question category.
|
|
|
380 |
$questions = $this->get_question_data(array_map(static fn($cat) => $cat->id, $allcoursecatcats));
|
|
|
381 |
$this->assertCount(2, $questions);
|
|
|
382 |
$question = reset($questions);
|
|
|
383 |
$this->assertEquals($parentcat->id, $question->categoryid);
|
|
|
384 |
// Make sure there are files in the expected fileareas for this question.
|
|
|
385 |
$fs = get_file_storage();
|
|
|
386 |
$this->assertTrue($fs->file_exists($this->coursecatcontext->id, 'question', 'questiontext', $question->id, '/', '1.png'));
|
|
|
387 |
$this->assertTrue(
|
|
|
388 |
$fs->file_exists($this->coursecatcontext->id, 'question', 'generalfeedback', $question->id, '/', '2.png'),
|
|
|
389 |
);
|
|
|
390 |
$this->assertTrue($fs->file_exists($this->coursecatcontext->id, 'qtype_essay', 'graderinfo', $question->id, '/', '3.png'));
|
|
|
391 |
|
|
|
392 |
// Make sure we have 4 question categories at course level (including 'top') with some questions in them.
|
|
|
393 |
$allcoursecats = $DB->get_records('question_categories', ['contextid' => $this->coursecontext->id], 'id ASC');
|
|
|
394 |
$this->assertCount(4, $allcoursecats);
|
|
|
395 |
$grandparentcat = next($allcoursecats);
|
|
|
396 |
$parentcat = next($allcoursecats);
|
|
|
397 |
$this->assertEquals($grandparentcat->id, $parentcat->parent);
|
|
|
398 |
$childcat = end($allcoursecats);
|
|
|
399 |
$this->assertEquals($parentcat->id, $childcat->parent);
|
|
|
400 |
$questions = $this->get_question_data(array_map(static fn($cat) => $cat->id, $allcoursecats));
|
|
|
401 |
// 2 active questions and 1 stale question, for a total of 3.
|
|
|
402 |
$this->assertCount(3, $questions);
|
|
|
403 |
|
|
|
404 |
// Make sure we have 6 stale question categories at course level (including 'top') with some questions in them.
|
|
|
405 |
$questioncats = $DB->get_records('question_categories', ['contextid' => $this->stalecoursecontext->id], 'id ASC');
|
|
|
406 |
$this->assertCount(6, $questioncats);
|
|
|
407 |
$topcat = reset($questioncats);
|
|
|
408 |
$parentcat1 = next($questioncats);
|
|
|
409 |
$childcat1 = next($questioncats);
|
|
|
410 |
$parentcat2 = next($questioncats);
|
|
|
411 |
$childcat2 = next($questioncats);
|
|
|
412 |
$grandchildcat1 = next($questioncats);
|
|
|
413 |
$this->assertEquals($topcat->id, $parentcat1->parent);
|
|
|
414 |
$this->assertEquals($topcat->id, $parentcat2->parent);
|
|
|
415 |
$this->assertEquals($parentcat1->id, $childcat1->parent);
|
|
|
416 |
$this->assertEquals($parentcat2->id, $childcat2->parent);
|
|
|
417 |
$this->assertEquals($childcat2->id, $grandchildcat1->parent);
|
|
|
418 |
// There should be 4 question bank entries with 1 version each, and 1 with 3 versions, for a total of 7.
|
|
|
419 |
$questionids = $this->get_question_data(array_map(static fn($cat) => $cat->id, $questioncats));
|
|
|
420 |
$this->assertCount(7, $questionids);
|
|
|
421 |
|
|
|
422 |
// Make sure the "Used-Unused-Empty" course has 4 question categories (including 'top') with 0, 2, 2, and 0
|
|
|
423 |
// questions respectively.
|
|
|
424 |
$questioncats = $DB->get_records('question_categories', ['contextid' => $this->usedunusedcontext->id], 'id ASC');
|
|
|
425 |
$this->assertCount(4, $questioncats);
|
|
|
426 |
$topcat = reset($questioncats);
|
|
|
427 |
$this->assertEmpty($this->get_question_data([$topcat->id]));
|
|
|
428 |
$usedcat = next($questioncats);
|
|
|
429 |
$this->assertCount(2, $this->get_question_data([$usedcat->id]));
|
|
|
430 |
$unusedcat = next($questioncats);
|
|
|
431 |
$this->assertCount(2, $this->get_question_data([$unusedcat->id]));
|
|
|
432 |
$emptycat = next($questioncats);
|
|
|
433 |
$this->assertCount(0, $this->get_question_data([$emptycat->id]));
|
|
|
434 |
|
|
|
435 |
// The question reference for the random question is using the "used" category, and the site context.
|
|
|
436 |
$coursemods = get_course_mods($this->usedunusedcontext->instanceid);
|
|
|
437 |
$quiz = reset($coursemods);
|
|
|
438 |
$quizsettings = quiz_settings::create($quiz->instance);
|
|
|
439 |
$structure = $quizsettings->get_structure();
|
|
|
440 |
$randomquestionslot = $structure->get_question_in_slot(2);
|
|
|
441 |
$this->assertEquals($this->usedunusedcontext->id, $randomquestionslot->contextid);
|
|
|
442 |
$this->assertEquals($usedcat->id, $randomquestionslot->filtercondition['filter']['category']['values'][0]);
|
|
|
443 |
}
|
|
|
444 |
|
|
|
445 |
/**
|
|
|
446 |
* Assert the installation task handles the deprecated contexts correctly.
|
|
|
447 |
*
|
|
|
448 |
* @return void
|
|
|
449 |
*/
|
|
|
450 |
public function test_qbank_install(): void {
|
|
|
451 |
global $DB;
|
|
|
452 |
$this->resetAfterTest();
|
|
|
453 |
$this->setup_pre_install_data();
|
|
|
454 |
|
|
|
455 |
$task = new transfer_question_categories();
|
|
|
456 |
$task->execute();
|
|
|
457 |
|
|
|
458 |
// Site context checks.
|
|
|
459 |
|
|
|
460 |
$sitecontext = context_system::instance();
|
|
|
461 |
$sitecontextcats = $DB->get_records('question_categories', ['contextid' => $sitecontext->id]);
|
|
|
462 |
|
|
|
463 |
// Should be no site context question categories left, not even 'top'.
|
|
|
464 |
$this->assertCount(0, $sitecontextcats);
|
|
|
465 |
|
|
|
466 |
$sitemodinfo = get_fast_modinfo(get_site());
|
|
|
467 |
$siteqbanks = $sitemodinfo->get_instances_of('qbank');
|
|
|
468 |
|
|
|
469 |
// We should have 1 new module on the site course.
|
|
|
470 |
$this->assertCount(1, $siteqbanks);
|
|
|
471 |
$siteqbank = reset($siteqbanks);
|
|
|
472 |
|
|
|
473 |
// Make doubly sure it got put into section 0 as these mod types are not rendered to the course page.
|
|
|
474 |
$this->assertEquals(0, $siteqbank->sectionnum);
|
|
|
475 |
|
|
|
476 |
// It should have our determined name.
|
|
|
477 |
$this->assertEquals('System shared question bank', $siteqbank->name);
|
|
|
478 |
$sitemodcontext = context_module::instance($siteqbank->get_course_module_record()->id);
|
|
|
479 |
|
|
|
480 |
// The 3 question categories including 'top' should now be at the new module context with their order intact.
|
|
|
481 |
$sitemodcats = $DB->get_records_select('question_categories',
|
|
|
482 |
'parent <> 0 AND contextid = :contextid',
|
|
|
483 |
['contextid' => $sitemodcontext->id],
|
|
|
484 |
'id ASC'
|
|
|
485 |
);
|
|
|
486 |
$this->assertCount(2, $sitemodcats);
|
|
|
487 |
$topcat = question_get_top_category($sitemodcontext->id);
|
|
|
488 |
$parentcat = reset($sitemodcats);
|
|
|
489 |
$childcat = next($sitemodcats);
|
|
|
490 |
$this->assertEquals($topcat->id, $parentcat->parent);
|
|
|
491 |
$this->assertEquals($parentcat->id, $childcat->parent);
|
|
|
492 |
|
|
|
493 |
// The random question should now point to the questions in the site course question bank.
|
|
|
494 |
$randomcourse = $DB->get_record('course', ['shortname' => 'Random']);
|
|
|
495 |
$coursemods = get_course_mods($randomcourse->id);
|
|
|
496 |
$randomquiz = reset($coursemods);
|
|
|
497 |
$randomquizsettings = quiz_settings::create($randomquiz->instance);
|
|
|
498 |
$structure = $randomquizsettings->get_structure();
|
|
|
499 |
$randomquestionslot = $structure->get_question_in_slot(1);
|
|
|
500 |
$this->assertEquals($randomquestionslot->contextid, $sitemodcontext->id);
|
|
|
501 |
$loader = new random_question_loader(new \qubaid_list([]));
|
|
|
502 |
$randomquestions = $loader->get_filtered_questions($randomquestionslot->filtercondition['filter']);
|
|
|
503 |
$this->assertCount(2, $randomquestions);
|
|
|
504 |
$randomq1 = reset($randomquestions);
|
|
|
505 |
$randomq2 = end($randomquestions);
|
|
|
506 |
$this->assertEquals($parentcat->id, $randomq1->category);
|
|
|
507 |
$this->assertEquals($childcat->id, $randomq2->category);
|
|
|
508 |
|
|
|
509 |
// Course category context checks.
|
|
|
510 |
|
|
|
511 |
// Make sure that the course category has no question categories, not even 'top'.
|
|
|
512 |
$this->assertEquals(0, $DB->count_records('question_categories', ['contextid' => $this->coursecatcontext->id]));
|
|
|
513 |
|
|
|
514 |
$courses = $DB->get_records('course', ['category' => $this->coursecatcontext->instanceid], 'id ASC');
|
|
|
515 |
// We should have 2 courses in this category now, the original and the new one that holds our new mod instance.
|
|
|
516 |
$this->assertCount(2, $courses);
|
|
|
517 |
$newcourse = end($courses);
|
|
|
518 |
$coursecat = $DB->get_record('course_categories', ['id' => $newcourse->category]);
|
|
|
519 |
|
|
|
520 |
// Make sure the new course shortname is a unique name based on the category name and id.
|
|
|
521 |
$this->assertEquals("$coursecat->name-$coursecat->id", $newcourse->shortname);
|
|
|
522 |
|
|
|
523 |
// Make sure the new course fullname is based on the category name.
|
|
|
524 |
$this->assertEquals("Shared teaching resources for category: $coursecat->name", $newcourse->fullname);
|
|
|
525 |
|
|
|
526 |
$coursemodinfo = get_fast_modinfo($newcourse);
|
|
|
527 |
$coursecatqbanks = $coursemodinfo->get_instances_of('qbank');
|
|
|
528 |
|
|
|
529 |
// We should have 1 new module on this course.
|
|
|
530 |
$this->assertCount(1, $coursecatqbanks);
|
|
|
531 |
$coursecatqbank = reset($coursecatqbanks);
|
|
|
532 |
|
|
|
533 |
// Make sure the new module name is what we expect.
|
|
|
534 |
$this->assertEquals("$coursecat->name shared question bank", $coursecatqbank->name);
|
|
|
535 |
|
|
|
536 |
$coursecatqcats = $DB->get_records('question_categories', ['contextid' => $coursecatqbank->context->id], 'parent ASC');
|
|
|
537 |
|
|
|
538 |
// The 2 question categories should be moved to the module context now.
|
|
|
539 |
$this->assertCount(2, $coursecatqcats);
|
|
|
540 |
$topcat = reset($coursecatqcats);
|
|
|
541 |
$parentcat = end($coursecatqcats);
|
|
|
542 |
|
|
|
543 |
// Make sure the parent orders are correct.
|
|
|
544 |
$this->assertEquals($topcat->id, $parentcat->parent);
|
|
|
545 |
|
|
|
546 |
// Course context checks.
|
|
|
547 |
|
|
|
548 |
// Make sure that the course has no more question categories, not even 'top'.
|
|
|
549 |
$this->assertEquals(0, $DB->count_records('question_categories', ['contextid' => $this->coursecontext->id]));
|
|
|
550 |
|
|
|
551 |
$coursemodinfo = get_fast_modinfo($this->coursecontext->instanceid);
|
|
|
552 |
$course = $coursemodinfo->get_course();
|
|
|
553 |
$courseqbanks = $coursemodinfo->get_instances_of('qbank');
|
|
|
554 |
|
|
|
555 |
// We should have only 1 new mod instance in this course.
|
|
|
556 |
$this->assertCount(1, $coursecatqbanks);
|
|
|
557 |
|
|
|
558 |
// The module name should be what we expect.
|
|
|
559 |
$courseqbank = reset($courseqbanks);
|
|
|
560 |
$this->assertEquals("$course->shortname shared question bank", $courseqbank->name);
|
|
|
561 |
|
|
|
562 |
// Make sure the question categories still exist and that we have a new top one at the new module context.
|
|
|
563 |
$topcat = question_get_top_category($courseqbank->context->id);
|
|
|
564 |
$courseqcats = $DB->get_records_select('question_categories',
|
|
|
565 |
'parent <> 0 AND contextid = :contextid',
|
|
|
566 |
['contextid' => $courseqbank->context->id],
|
|
|
567 |
'id ASC'
|
|
|
568 |
);
|
|
|
569 |
$grandparentcat = reset($courseqcats);
|
|
|
570 |
$parentcat = next($courseqcats);
|
|
|
571 |
$childcat = next($courseqcats);
|
|
|
572 |
|
|
|
573 |
$this->assertEquals($topcat->id, $grandparentcat->parent);
|
|
|
574 |
$this->assertEquals($grandparentcat->id, $parentcat->parent);
|
|
|
575 |
$this->assertEquals($parentcat->id, $childcat->parent);
|
|
|
576 |
// Make sure the two active questions were migrated with their categories, but not the stale question.
|
|
|
577 |
$migratedquestions = $this->get_question_data([$parentcat->id, $childcat->id]);
|
|
|
578 |
$this->assertCount(2, $migratedquestions);
|
|
|
579 |
foreach ($migratedquestions as $migratedquestion) {
|
|
|
580 |
$this->assertTrue($migratedquestion->status === question_version_status::QUESTION_STATUS_READY);
|
|
|
581 |
}
|
|
|
582 |
|
|
|
583 |
// Stale course context checks.
|
|
|
584 |
|
|
|
585 |
// Make sure the stale course has no categories attached to it anymore and the questions were removed.
|
|
|
586 |
$this->assertFalse($DB->record_exists('question_categories', ['contextid' => $this->stalecoursecontext->id]));
|
|
|
587 |
foreach ($this->stalequestions as $stalequestion) {
|
|
|
588 |
$this->assertFalse($DB->record_exists('question', ['id' => $stalequestion->id]));
|
|
|
589 |
}
|
|
|
590 |
// Make sure the we did not create a qbank in the stale course.
|
|
|
591 |
$this->assertEmpty(get_fast_modinfo($this->stalecoursecontext->instanceid)->get_instances_of('qbank'));
|
|
|
592 |
|
|
|
593 |
// Quiz module checks.
|
|
|
594 |
|
|
|
595 |
// Make sure the 3 categories at quiz context, including 'top' have not been touched.
|
|
|
596 |
$quizcategories = $DB->get_records('question_categories', ['contextid' => $this->quizcontext->id]);
|
|
|
597 |
$this->assertCount(3, $quizcategories);
|
|
|
598 |
$questions = $this->get_question_data(array_map(static fn($cat) => $cat->id, $quizcategories));
|
|
|
599 |
$this->assertCount(2, $questions);
|
|
|
600 |
|
|
|
601 |
// Used-Unused-Empty checks.
|
|
|
602 |
// The empty category should have been removed. The other categories should both have been migrated to a qbank module,
|
|
|
603 |
// with all of their questions.
|
|
|
604 |
$usedunusedmodinfo = get_fast_modinfo($this->usedunusedcontext->instanceid);
|
|
|
605 |
$usedunusedcourse = $usedunusedmodinfo->get_course();
|
|
|
606 |
$usedunusedqbanks = $usedunusedmodinfo->get_instances_of('qbank');
|
|
|
607 |
$usedunusedqbank = reset($usedunusedqbanks);
|
|
|
608 |
$this->assertEquals("$usedunusedcourse->shortname shared question bank", $usedunusedqbank->name);
|
|
|
609 |
|
|
|
610 |
// We should now only have 3 categories. Top, used and unused.
|
|
|
611 |
$usedunusedcats = $DB->get_records(
|
|
|
612 |
'question_categories',
|
|
|
613 |
['contextid' => $usedunusedqbank->context->id],
|
|
|
614 |
fields: 'name, id',
|
|
|
615 |
);
|
|
|
616 |
$this->assertCount(3, $usedunusedcats);
|
|
|
617 |
$this->assertArrayHasKey('top', $usedunusedcats);
|
|
|
618 |
$this->assertArrayHasKey('Used Question Cat', $usedunusedcats);
|
|
|
619 |
$this->assertArrayHasKey('Unused Question Cat', $usedunusedcats);
|
|
|
620 |
$this->assertArrayNotHasKey('Empty Question Cat', $usedunusedcats);
|
|
|
621 |
|
|
|
622 |
$this->assertEmpty($this->get_question_data([$usedunusedcats['top']->id]));
|
|
|
623 |
$this->assertCount(2, $this->get_question_data([$usedunusedcats['Used Question Cat']->id]));
|
|
|
624 |
$this->assertCount(2, $this->get_question_data([$usedunusedcats['Unused Question Cat']->id]));
|
|
|
625 |
|
|
|
626 |
// The question reference for the random question is using the same category, but the new context.
|
|
|
627 |
$modinfo = get_fast_modinfo($this->usedunusedcontext->instanceid);
|
|
|
628 |
$quizzes = $modinfo->get_instances_of('quiz');
|
|
|
629 |
$quiz = reset($quizzes);
|
|
|
630 |
$quizsettings = quiz_settings::create($quiz->instance);
|
|
|
631 |
$structure = $quizsettings->get_structure();
|
|
|
632 |
$randomquestionslot = $structure->get_question_in_slot(2);
|
|
|
633 |
$this->assertEquals($usedunusedqbank->context->id, $randomquestionslot->contextid);
|
|
|
634 |
$this->assertEquals(
|
|
|
635 |
$usedunusedcats['Used Question Cat']->id,
|
|
|
636 |
$randomquestionslot->filtercondition['filter']['category']['values'][0]
|
|
|
637 |
);
|
|
|
638 |
}
|
|
|
639 |
|
|
|
640 |
/**
|
|
|
641 |
* Assert the installation task handles the missing contexts correctly.
|
|
|
642 |
*
|
|
|
643 |
* @return void
|
|
|
644 |
*/
|
|
|
645 |
public function test_qbank_install_with_missing_context(): void {
|
|
|
646 |
global $DB;
|
|
|
647 |
$this->resetAfterTest();
|
|
|
648 |
self::setAdminUser();
|
|
|
649 |
|
|
|
650 |
$questiongenerator = self::getDataGenerator()->get_plugin_generator('core_question');
|
|
|
651 |
|
|
|
652 |
// The problem is that question categories that used to related to contextids
|
|
|
653 |
// which no longer exist are now all moved to the new system-level shared
|
|
|
654 |
// question bank. This moving categories together can cause unique key violations.
|
|
|
655 |
|
|
|
656 |
// Create 2 orphaned categories where the contextid no longer exists, with the same stamp and idnumber.
|
|
|
657 |
// We need to do this by creating in a real context, then deleting the context,
|
|
|
658 |
// because create category logs, which needs a valid context id.
|
|
|
659 |
$tamperedstamp = make_unique_id_code();
|
|
|
660 |
$context1 = context_course::instance(self::getDataGenerator()->create_course()->id);
|
|
|
661 |
$oldcat1 = $this->create_question_category('Lost category 1', $context1->id);
|
|
|
662 |
$oldcat1->stamp = $tamperedstamp;
|
|
|
663 |
$oldcat1->idnumber = 'tamperedidnumber';
|
|
|
664 |
$DB->update_record('question_categories', $oldcat1);
|
|
|
665 |
$DB->delete_records('context', ['id' => $context1->id]);
|
|
|
666 |
|
|
|
667 |
$context2 = context_course::instance(self::getDataGenerator()->create_course()->id);
|
|
|
668 |
$oldcat2 = $this->create_question_category('Lost category 2', $context2->id);
|
|
|
669 |
$oldcat2->stamp = $tamperedstamp;
|
|
|
670 |
$oldcat2->idnumber = 'tamperedidnumber';
|
|
|
671 |
$DB->update_record('question_categories', $oldcat2);
|
|
|
672 |
$DB->delete_records('context', ['id' => $context2->id]);
|
|
|
673 |
|
|
|
674 |
// Add a question to each category.
|
|
|
675 |
$question1 = $questiongenerator->create_question('shortanswer', null, ['category' => $oldcat1->id]);
|
|
|
676 |
$question2 = $questiongenerator->create_question('shortanswer', null, ['category' => $oldcat2->id]);
|
|
|
677 |
|
|
|
678 |
// Make the questions 'in use'.
|
|
|
679 |
$quizcourse = self::getDataGenerator()->create_course();
|
|
|
680 |
$quiz = self::getDataGenerator()->get_plugin_generator('mod_quiz')->create_instance(
|
|
|
681 |
['course' => $quizcourse->id, 'grade' => 100.0, 'sumgrades' => 2, 'layout' => '1,0']
|
|
|
682 |
);
|
|
|
683 |
quiz_add_quiz_question($question1->id, $quiz);
|
|
|
684 |
quiz_add_quiz_question($question2->id, $quiz);
|
|
|
685 |
|
|
|
686 |
// Make sure the caches are reset so that the contexts are not cached.
|
|
|
687 |
\core\context_helper::reset_caches();
|
|
|
688 |
|
|
|
689 |
// Run the task.
|
|
|
690 |
$task = new transfer_question_categories();
|
|
|
691 |
$task->execute();
|
|
|
692 |
// An important thing to verify is that the task completes without errors,
|
|
|
693 |
// for example unique key violations.
|
|
|
694 |
|
|
|
695 |
// Verify - there should be a single question bank in the site course with the expected name.
|
|
|
696 |
$sitemodinfo = get_fast_modinfo(get_site());
|
|
|
697 |
$siteqbanks = $sitemodinfo->get_instances_of('qbank');
|
|
|
698 |
$this->assertCount(1, $siteqbanks);
|
|
|
699 |
$siteqbank = reset($siteqbanks);
|
|
|
700 |
$this->assertEquals('System shared question bank', $siteqbank->name);
|
|
|
701 |
|
|
|
702 |
// The two previously orphaned categories should now be in this site questions bank, with a top category.
|
|
|
703 |
$sitemodcontext = context_module::instance($siteqbank->get_course_module_record()->id);
|
|
|
704 |
$sitemodcats = $DB->get_records_select(
|
|
|
705 |
'question_categories',
|
|
|
706 |
'parent <> 0 AND contextid = :contextid',
|
|
|
707 |
['contextid' => $sitemodcontext->id],
|
|
|
708 |
'id ASC',
|
|
|
709 |
);
|
|
|
710 |
|
|
|
711 |
// Work out which category is which.
|
|
|
712 |
$movedcat1 = null;
|
|
|
713 |
$movedcat2 = null;
|
|
|
714 |
foreach ($sitemodcats as $movedcat) {
|
|
|
715 |
if ($movedcat->name === $oldcat1->name) {
|
|
|
716 |
$movedcat1 = $movedcat;
|
|
|
717 |
}
|
|
|
718 |
if ($movedcat->name === $oldcat2->name) {
|
|
|
719 |
$movedcat2 = $movedcat;
|
|
|
720 |
}
|
|
|
721 |
}
|
|
|
722 |
$this->assertNotNull($movedcat1);
|
|
|
723 |
$this->assertNotNull($movedcat2);
|
|
|
724 |
|
|
|
725 |
// Verify the properties of the moved categories.
|
|
|
726 |
$this->assertNotEquals($movedcat1->stamp, $movedcat2->stamp);
|
|
|
727 |
$this->assertNotEquals($movedcat1->idnumber, $movedcat2->idnumber);
|
|
|
728 |
$this->assertEquals(question_get_top_category($sitemodcontext->id)->id, $movedcat1->parent);
|
|
|
729 |
$this->assertEquals(question_get_top_category($sitemodcontext->id)->id, $movedcat2->parent);
|
|
|
730 |
}
|
|
|
731 |
|
|
|
732 |
public function test_fix_wrong_parents(): void {
|
|
|
733 |
$this->resetAfterTest();
|
|
|
734 |
$this->setup_pre_install_data();
|
|
|
735 |
|
|
|
736 |
// Create a second course.
|
|
|
737 |
$course2 = self::getDataGenerator()->create_course();
|
|
|
738 |
$course2context = context_course::instance($course2->id);
|
|
|
739 |
|
|
|
740 |
// In course2 we build this category structure:
|
|
|
741 |
// - $course2parentcat -- context $course2context
|
|
|
742 |
// - - $wrongchild1 -- context $this->coursecontext (wrong)
|
|
|
743 |
// - - - $wronggrandchild1 -- context $this->coursecontext (same wrong)
|
|
|
744 |
// - - - $doublywronggrandchild1 -- context $course2context (back right, but not matching its parent)
|
|
|
745 |
// - - $wrongchild2 -- context non-existant A
|
|
|
746 |
// - - - $wronggrandchild2 -- context non-existent A
|
|
|
747 |
// - - - $doublywronggrandchild2 -- context non-existent B.
|
|
|
748 |
$course2parentcat = $this->create_question_category(
|
|
|
749 |
'Course2 parent cat', $course2context->id);
|
|
|
750 |
|
|
|
751 |
$wrongchild1 = $this->create_question_category(
|
|
|
752 |
'Child cat with wrong context', $this->coursecontext->id, $course2parentcat->id);
|
|
|
753 |
$wronggrandchild1 = $this->create_question_category(
|
|
|
754 |
'Grandchild of child1 in same wrong context', $this->coursecontext->id, $wrongchild1->id);
|
|
|
755 |
$doublywronggrandchild1 = $this->create_question_category(
|
|
|
756 |
'Grandchild of child1 back in the right context', $course2context->id, $wrongchild1->id);
|
|
|
757 |
|
|
|
758 |
$wrongchild2 = $this->create_question_category(
|
|
|
759 |
'Child cat with non-existent context', $course2context->id + 1000, $course2parentcat->id);
|
|
|
760 |
$wronggrandchild2 = $this->create_question_category(
|
|
|
761 |
'Grandchild of child2 with same non-existent context', $course2context->id + 1000, $wrongchild2->id);
|
|
|
762 |
$doublywronggrandchild2 = $this->create_question_category(
|
|
|
763 |
'Grandchild of child2 with different non-existent context', $course2context->id + 2000, $wrongchild2->id);
|
|
|
764 |
|
|
|
765 |
// Before we clean up, check that the expected categories are picked up.
|
|
|
766 |
// $wronggrandchild1 & $wronggrandchild2 are not seen, because their contexts match
|
|
|
767 |
// their parent's even though both are wrong. They should still get fixed.
|
|
|
768 |
$task = new transfer_question_categories();
|
|
|
769 |
$this->assertEquals(
|
|
|
770 |
[
|
|
|
771 |
$wrongchild1->id => $wrongchild1->contextid,
|
|
|
772 |
$doublywronggrandchild1->id => $course2context->id,
|
|
|
773 |
$wrongchild2->id => $wrongchild2->contextid,
|
|
|
774 |
$doublywronggrandchild2->id => $doublywronggrandchild2->contextid,
|
|
|
775 |
],
|
|
|
776 |
$task->get_categories_in_a_different_context_to_their_parent(),
|
|
|
777 |
);
|
|
|
778 |
|
|
|
779 |
// Call the cleanup method.
|
|
|
780 |
$task->fix_wrong_parents();
|
|
|
781 |
|
|
|
782 |
// Now we expect no mismatches.
|
|
|
783 |
$this->assertEmpty($task->get_categories_in_a_different_context_to_their_parent());
|
|
|
784 |
|
|
|
785 |
// Assert that the child categories have been moved to the locations they should have been.
|
|
|
786 |
$this->assert_category_is_in_context_with_parent($this->coursecontext, null, $wrongchild1->id);
|
|
|
787 |
$this->assert_category_is_in_context_with_parent($this->coursecontext, $wrongchild1, $wronggrandchild1->id);
|
|
|
788 |
$this->assert_category_is_in_context_with_parent($course2context, null, $doublywronggrandchild1->id);
|
|
|
789 |
$this->assert_category_is_in_context_with_parent($course2context, $course2parentcat, $wrongchild2->id);
|
|
|
790 |
$this->assert_category_is_in_context_with_parent($course2context, $wrongchild2, $wronggrandchild2->id);
|
|
|
791 |
$this->assert_category_is_in_context_with_parent($course2context, $wrongchild2, $doublywronggrandchild2->id);
|
|
|
792 |
}
|
|
|
793 |
|
|
|
794 |
/**
|
|
|
795 |
* Assert that the category with id $categoryid is in context $expectedcontext, with the given parent.
|
|
|
796 |
*
|
|
|
797 |
* @param context $expectedcontext the expected context for the category with id $categoryid.
|
|
|
798 |
* @param stdClass|null $expectedparent the expected parent category.
|
|
|
799 |
* null means the Top category in $expectedcontext.
|
|
|
800 |
* @param int $categoryid the id of the category to check.
|
|
|
801 |
*/
|
|
|
802 |
protected function assert_category_is_in_context_with_parent(
|
|
|
803 |
context $expectedcontext,
|
|
|
804 |
?stdClass $expectedparent,
|
|
|
805 |
int $categoryid,
|
|
|
806 |
): void {
|
|
|
807 |
global $DB;
|
|
|
808 |
|
|
|
809 |
if ($expectedparent === null) {
|
|
|
810 |
$expectedparent = $DB->get_record(
|
|
|
811 |
'question_categories',
|
|
|
812 |
['contextid' => $expectedcontext->id, 'parent' => 0],
|
|
|
813 |
'*',
|
|
|
814 |
MUST_EXIST,
|
|
|
815 |
);
|
|
|
816 |
}
|
|
|
817 |
|
|
|
818 |
$actualcategory = $DB->get_record('question_categories', ['id' => $categoryid]);
|
|
|
819 |
$this->assertEquals($expectedparent->id, $actualcategory->parent,
|
|
|
820 |
"Checking parent of category $actualcategory->name.");
|
|
|
821 |
$this->assertEquals($expectedcontext->id, $actualcategory->contextid,
|
|
|
822 |
"Checking context of category $actualcategory->name.");
|
|
|
823 |
}
|
|
|
824 |
|
|
|
825 |
public function test_transfer_questions(): void {
|
|
|
826 |
global $DB;
|
|
|
827 |
$this->resetAfterTest();
|
|
|
828 |
$this->setup_pre_install_data();
|
|
|
829 |
|
|
|
830 |
$task = new \mod_qbank\task\transfer_question_categories();
|
|
|
831 |
$task->execute();
|
|
|
832 |
|
|
|
833 |
// Assert that files are still in their original context.
|
|
|
834 |
$courses = $DB->get_records('course', ['category' => $this->coursecatcontext->instanceid], 'id ASC');
|
|
|
835 |
$newcourse = end($courses);
|
|
|
836 |
$coursemodinfo = get_fast_modinfo($newcourse);
|
|
|
837 |
$coursecatqbanks = $coursemodinfo->get_instances_of('qbank');
|
|
|
838 |
$coursecatqbank = reset($coursecatqbanks);
|
|
|
839 |
$coursecatqcats = $DB->get_records('question_categories', ['contextid' => $coursecatqbank->context->id], 'parent ASC');
|
|
|
840 |
$parentcat = end($coursecatqcats);
|
|
|
841 |
$questions = get_questions_category($parentcat, true);
|
|
|
842 |
$question = reset($questions);
|
|
|
843 |
$fs = get_file_storage();
|
|
|
844 |
$this->assertTrue($fs->file_exists(
|
|
|
845 |
$this->coursecatcontext->id,
|
|
|
846 |
'question',
|
|
|
847 |
'questiontext',
|
|
|
848 |
$question->id,
|
|
|
849 |
'/',
|
|
|
850 |
'1.png'
|
|
|
851 |
));
|
|
|
852 |
$this->assertTrue($fs->file_exists(
|
|
|
853 |
$this->coursecatcontext->id,
|
|
|
854 |
'question',
|
|
|
855 |
'generalfeedback',
|
|
|
856 |
$question->id,
|
|
|
857 |
'/',
|
|
|
858 |
'2.png'
|
|
|
859 |
));
|
|
|
860 |
$this->assertTrue($fs->file_exists(
|
|
|
861 |
$this->coursecatcontext->id,
|
|
|
862 |
'qtype_essay',
|
|
|
863 |
'graderinfo',
|
|
|
864 |
$question->id,
|
|
|
865 |
'/',
|
|
|
866 |
'3.png'
|
|
|
867 |
));
|
|
|
868 |
$this->assertFalse($fs->file_exists(
|
|
|
869 |
$coursecatqbank->context->id,
|
|
|
870 |
'question',
|
|
|
871 |
'questiontext',
|
|
|
872 |
$question->id,
|
|
|
873 |
'/',
|
|
|
874 |
'1.png'
|
|
|
875 |
));
|
|
|
876 |
$this->assertFalse($fs->file_exists(
|
|
|
877 |
$coursecatqbank->context->id,
|
|
|
878 |
'question',
|
|
|
879 |
'generalfeedback',
|
|
|
880 |
$question->id,
|
|
|
881 |
'/',
|
|
|
882 |
'2.png'
|
|
|
883 |
));
|
|
|
884 |
$this->assertFalse($fs->file_exists(
|
|
|
885 |
$coursecatqbank->context->id,
|
|
|
886 |
'qtype_essay',
|
|
|
887 |
'graderinfo',
|
|
|
888 |
$question->id,
|
|
|
889 |
'/',
|
|
|
890 |
'3.png'
|
|
|
891 |
));
|
|
|
892 |
|
|
|
893 |
$this->assertFalse(question_bank_helper::has_bank_migration_task_completed_successfully());
|
|
|
894 |
|
|
|
895 |
$questiontasks = manager::get_adhoc_tasks(transfer_questions::class);
|
|
|
896 |
|
|
|
897 |
// We should have a transfer_questions task for each category that was moved.
|
|
|
898 |
// 2 site categories,
|
|
|
899 |
// 1 coursecat category,
|
|
|
900 |
// 3 regular course categories,
|
|
|
901 |
// 2 used/unused course categories.
|
|
|
902 |
$this->assertCount(8, $questiontasks);
|
|
|
903 |
|
|
|
904 |
$this->expectOutputRegex('~Moving files and tags~');
|
|
|
905 |
// Delete one of the categories before running the tasks, to ensure missing categories are handled gracefully.
|
|
|
906 |
$unusedcat = $DB->get_record('question_categories', ['name' => 'Unused Question Cat']);
|
|
|
907 |
question_category_delete_safe($unusedcat);
|
|
|
908 |
$this->expectOutputRegex("~Could not find a category record for id {$unusedcat->id}. Terminating task.~");
|
|
|
909 |
|
|
|
910 |
$this->runAdhocTasks();
|
|
|
911 |
|
|
|
912 |
// The files have now been moved to the new context.
|
|
|
913 |
$this->assertFalse($fs->file_exists(
|
|
|
914 |
$this->coursecatcontext->id,
|
|
|
915 |
'question',
|
|
|
916 |
'questiontext',
|
|
|
917 |
$question->id,
|
|
|
918 |
'/',
|
|
|
919 |
'1.png'
|
|
|
920 |
));
|
|
|
921 |
$this->assertFalse($fs->file_exists(
|
|
|
922 |
$this->coursecatcontext->id,
|
|
|
923 |
'question',
|
|
|
924 |
'generalfeedback',
|
|
|
925 |
$question->id,
|
|
|
926 |
'/',
|
|
|
927 |
'2.png'
|
|
|
928 |
));
|
|
|
929 |
$this->assertFalse($fs->file_exists(
|
|
|
930 |
$this->coursecatcontext->id,
|
|
|
931 |
'qtype_essay',
|
|
|
932 |
'graderinfo',
|
|
|
933 |
$question->id,
|
|
|
934 |
'/',
|
|
|
935 |
'3.png'
|
|
|
936 |
));
|
|
|
937 |
$this->assertTrue($fs->file_exists(
|
|
|
938 |
$coursecatqbank->context->id,
|
|
|
939 |
'question',
|
|
|
940 |
'questiontext',
|
|
|
941 |
$question->id,
|
|
|
942 |
'/',
|
|
|
943 |
'1.png'
|
|
|
944 |
));
|
|
|
945 |
$this->assertTrue($fs->file_exists(
|
|
|
946 |
$coursecatqbank->context->id,
|
|
|
947 |
'question',
|
|
|
948 |
'generalfeedback',
|
|
|
949 |
$question->id,
|
|
|
950 |
'/',
|
|
|
951 |
'2.png'
|
|
|
952 |
));
|
|
|
953 |
$this->assertTrue($fs->file_exists(
|
|
|
954 |
$coursecatqbank->context->id,
|
|
|
955 |
'qtype_essay',
|
|
|
956 |
'graderinfo',
|
|
|
957 |
$question->id,
|
|
|
958 |
'/',
|
|
|
959 |
'3.png'
|
|
|
960 |
));
|
|
|
961 |
|
|
|
962 |
$this->assertTrue(question_bank_helper::has_bank_migration_task_completed_successfully());
|
|
|
963 |
}
|
|
|
964 |
}
|