MDL-71378 mod_qbank: Add install script to transfer question categories

This commit is contained in:
Simon Adams 2024-08-02 10:00:25 +01:00
parent 776d1e40e4
commit 1db7518a7d
6 changed files with 652 additions and 7 deletions

View File

@ -266,16 +266,14 @@ function question_category_delete_safe($category): void {
}
}
if (!empty($questionids)) {
$parentcontextid = SYSCONTEXTID;
$name = get_string('unknown', 'question');
if ($context !== false) {
$name = $context->get_context_name();
$parentcontext = $context->get_parent_context();
if ($parentcontext) {
$parentcontextid = $parentcontext->id;
}
$parentcontext = $context->get_course_context(false);
$course = $parentcontext ? get_course($parentcontext->instanceid) : get_site();
}
question_save_from_deletion(array_keys($questionids), $parentcontextid, $name, $rescue);
$qbank = core_question\local\bank\question_bank_helper::get_default_open_instance_system_type($course, true);
question_save_from_deletion(array_keys($questionids), $qbank->context->id, $name, $rescue);
}
}

View File

@ -0,0 +1,169 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_qbank\task;
use context_system;
use core\context;
use core\task\adhoc_task;
use core\task\manager;
use core_course_category;
use core_question\local\bank\question_bank_helper;
use stdClass;
/**
* /**
* This script transfers question categories at CONTEXT_SITE, CONTEXT_COURSE, & CONTEXT_COURSECAT to a new qbank instance
* context.
*
* Firstly, it finds any question categories where questions are not being used and deletes them, including questions.
*
* Then for any remaining, if it is at course level context, it creates a mod_qbank instance taking the course name
* and moves the category there including subcategories, files and tags.
*
* If the original question category context was at system context, then it creates a mod_qbank instance on the site course i.e.
* front page and moves the category & sub categories there, along with its files and tags.
*
* If the original question category context was a course category context, then it creates a course in that category,
* taking the category name. Then it creates a mod_qbank instance in that course and moves the category & sub categories
* there, along with files and tags belonging to those categories.
*
* @package mod_qbank
* @copyright 2024 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Simon Adams <simon.adams@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class transfer_question_categories extends adhoc_task {
/**
* Run the install task.
*
* @return void
*/
public function execute() {
global $DB, $CFG;
require_once($CFG->dirroot . '/course/modlib.php');
require_once($CFG->libdir . '/questionlib.php');
$recordset = $DB->get_recordset('question_categories', ['parent' => 0]);
foreach ($recordset as $oldtopcategory) {
if (!$oldcontext = context::instance_by_id($oldtopcategory->contextid, IGNORE_MISSING)) {
// That context does not exist anymore, we will treat these as if they were at site context level.
$oldcontext = context_system::instance();
}
$trans = $DB->start_delegated_transaction();
// Remove any unused questions if they are marked as deleted.
// Also, if a category contained questions which were all unusable then delete it as well.
$subcategories = $DB->get_records_select('question_categories',
'parent <> 0 AND contextid = :contextid',
['contextid' => $oldtopcategory->contextid]
);
// This gives us categories in parent -> child order so array_reverse it,
// because we should process stale categories from the bottom up.
$subcategories = array_reverse(\sort_categories_by_tree($subcategories, $oldtopcategory->id));
foreach ($subcategories as $subcategory) {
\qbank_managecategories\helper::question_remove_stale_questions_from_category($subcategory->id);
if (!question_category_in_use($subcategory->id)) {
question_category_delete_safe($subcategory);
}
}
// We don't want to transfer any categories at valid contexts i.e. quiz modules.
if ($oldcontext->contextlevel === CONTEXT_MODULE) {
$trans->allow_commit();
continue;
}
// Category is in use so let's process it. Firstly, a course and mod instance is needed.
switch ($oldcontext->contextlevel) {
case CONTEXT_SYSTEM:
$course = get_site();
$bankname = get_string('systembank', 'question');
break;
case CONTEXT_COURSECAT:
$coursecategory = core_course_category::get($oldcontext->instanceid);
$courseshortname = "{$coursecategory->name}-{$coursecategory->id}";
$course = $this->create_course($coursecategory, $courseshortname);
$bankname = get_string("sharedbank", "mod_qbank", $coursecategory->name);
break;
case CONTEXT_COURSE:
$course = get_course($oldcontext->instanceid);
$bankname = get_string("sharedbank", "mod_qbank", $course->shortname);
break;
default:
// This shouldn't be possible, so we can't really transfer it.
// We should commit any pre-transfer category cleanup though.
$trans->allow_commit();
continue 2;
}
if (!$newmod = question_bank_helper::get_default_open_instance_system_type($course)) {
$newmod = question_bank_helper::create_default_open_instance($course, $bankname, question_bank_helper::TYPE_SYSTEM);
}
// We have our new mod instance, now move all the subcategories of the old 'top' category to this new context.
$this->move_question_category($oldtopcategory, $newmod->context);
// Job done, lets delete the old 'top' category.
$DB->delete_records('question_categories', ['id' => $oldtopcategory->id]);
$trans->allow_commit();
}
$recordset->close();
}
/**
* Wrapper for \create_course.
*
* @param core_course_category $coursecategory
* @param string $shortname
* @return stdClass
*/
protected function create_course(\core_course_category $coursecategory, string $shortname): stdClass {
$data = (object) [
'enablecompletion' => 0,
'fullname' => get_string('coursecategory', 'mod_qbank', $coursecategory->name),
'shortname' => $shortname,
'category' => $coursecategory->id,
];
return \create_course($data);
}
/**
* Create a new 'Top' category in our new context and move the old categories descendents beneath it.
*
* @param stdClass $oldtopcategory The old 'Top' category that we are moving.
* @param \context $newcontext The context we are moving our category to.
* @return void
*/
protected function move_question_category(stdClass $oldtopcategory, \context $newcontext): void {
global $DB;
$newtopcategory = question_get_top_category($newcontext->id, true);
// This function moves subcategories, so we have to start at the top.
question_move_category_to_context($oldtopcategory->id, $oldtopcategory->contextid, $newcontext->id);
// Move the parent from the old top category to the new one.
$DB->set_field('question_categories', 'parent', $newtopcategory->id, ['parent' => $oldtopcategory->id]);
}
}

28
mod/qbank/db/install.php Normal file
View File

@ -0,0 +1,28 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* mod_qbank install operations to transfer question categories to new contexts.
*
* @package mod_qbank
* @copyright 2024 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Simon Adams <simon.adams@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
function xmldb_qbank_install(): void {
$task = new \mod_qbank\task\transfer_question_categories();
\core\task\manager::queue_adhoc_task($task);
}

View File

@ -13,6 +13,7 @@
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Timestamp of when the instance was last modified."/>
<FIELD NAME="intro" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Activity description."/>
<FIELD NAME="introformat" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The format of the intro field."/>
<FIELD NAME="type" TYPE="char" LENGTH="40" NOTNULL="true" SEQUENCE="false" COMMENT="The sub type of the activity module instance, e.g. standard, system, etc"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>

View File

@ -23,4 +23,15 @@
* @author Simon Adams <simon.adams@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class mod_qbank_generator extends testing_module_generator {}
class mod_qbank_generator extends testing_module_generator {
#[\Override]
public function create_instance($record = null, ?array $options = null) {
$record = (object)(array)$record;
if (empty($record->type)) {
$record->type = core_question\local\bank\question_bank_helper::TYPE_STANDARD;
}
return parent::create_instance($record, $options);
}
}

View File

@ -0,0 +1,438 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_qbank;
use context_course;
use context_coursecat;
use context_module;
use context_system;
use stdClass;
use core_question\local\bank\question_version_status;
/**
* Before testing, we firstly need to create some data to emulate what sites can have pre-upgrade.
* Namely, we are adding question categories and questions to deprecated contexts i.e. anything not CONTEXT_MODULE,
* and to quiz local banks too as we need to test these don't get touched.
* It also adds questions to some categories that are not used by quizzes anywhere.
*
* The tests cover a few areas.
* 1: We validate the data setup is correct before we run the installation script testing.
* 2: The installation test validates that any question categories not in CONTEXT_MODULE get transferred to relevant mod_qbank
* instances including their questions. It also validates that any stale questions that are not in use by quizzes are removed
* along with empty categories.
*
* @package mod_qbank
* @copyright 2024 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Simon Adams <simon.adams@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \mod_qbank\task\transfer_question_categories
*/
final class transfer_question_categories_test extends \advanced_testcase {
/** @var \core\context\coursecat test course category context */
private \core\context\coursecat $coursecatcontext;
/** @var \core\context\course test course context */
private \core\context\course $coursecontext;
/** @var \core\context\course test stale course context*/
private \core\context\course $stalecoursecontext;
/** @var \core\context\module test quiz mod context */
private \core\context\module $quizcontext;
/** @var stdClass[] test stale questions */
private array $stalequestions;
/**
* Get question data from question category ids provided in the argument.
*
* @param array $categoryids
* @return array
*/
protected function get_question_data(array $categoryids): array {
global $DB;
[$insql, $inparams] = $DB->get_in_or_equal($categoryids);
$sql = "SELECT q.id, qbe.questioncategoryid AS categoryid
FROM {question} q
JOIN {question_versions} qv ON qv.questionid = q.id
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
WHERE qbe.questioncategoryid {$insql}";
return $DB->get_records_sql($sql, $inparams);
}
/**
* This is hacky, but we can't use the API to create these as non module contexts are deprecated for holding question
* categories.
*
* @param string $name of the new category
* @param int $contextid of the module holding the category
* @param int $parentid of the new category
* @return stdClass category object
*/
protected function create_question_category(string $name, int $contextid, int $parentid = 0): stdClass {
global $DB;
if (!$parentid) {
if (!$parent = $DB->get_record('question_categories', ['contextid' => $contextid, 'parent' => 0, 'name' => 'top'])) {
$parent = new stdClass();
$parent->name = 'top';
$parent->info = '';
$parent->contextid = $contextid;
$parent->parent = 0;
$parent->sortorder = 0;
$parent->stamp = make_unique_id_code();
$parent->id = $DB->insert_record('question_categories', $parent);
}
$parentid = $parent->id;
}
$record = (object) [
'name' => $name,
'parent' => $parentid,
'contextid' => $contextid,
'info' => '',
'infoformat' => FORMAT_HTML,
'stamp' => make_unique_id_code(),
'sortorder' => 999,
'idnumber' => null,
];
$record->id = $DB->insert_record('question_categories', $record);
return $record;
}
/**
* Sets up the installation test with data.
*
* @return void
*/
protected function setup_pre_install_data(): void {
global $DB;
self::setAdminUser();
$questiongenerator = self::getDataGenerator()->get_plugin_generator('core_question');
$quizgenerator = self::getDataGenerator()->get_plugin_generator('mod_quiz');
// Setup 2 categories at site level context, with a question in each.
$sitecontext = context_system::instance();
$site = get_site();
$siteparentcat = $this->create_question_category('Site Parent Cat', $sitecontext->id);
$sitechildcat = $this->create_question_category('Site Child Cat', $sitecontext->id, $siteparentcat->id);
$question1 = $questiongenerator->create_question(
'shortanswer',
null,
['category' => $siteparentcat->id, 'status' => question_version_status::QUESTION_STATUS_READY]
);
$question2 = $questiongenerator->create_question(
'shortanswer',
null,
['category' => $sitechildcat->id, 'status' => question_version_status::QUESTION_STATUS_READY]
);
// Add a quiz to the site course and put those questions into it.
$quiz = $quizgenerator->create_instance(['course' => $site->id, 'grade' => 100.0, 'sumgrades' => 2, 'layout' => '1,0']);
quiz_add_quiz_question($question1->id, $quiz, 1);
quiz_add_quiz_question($question2->id, $quiz, 1);
// Create a course category and then a question category attached to that context.
$coursecategory = self::getDataGenerator()->create_category();
$this->coursecatcontext = context_coursecat::instance($coursecategory->id);
$coursecatcat = $this->create_question_category('Course Cat Parent Cat', $this->coursecatcontext->id);
// Add a question to the category just made.
$question3 = $questiongenerator->create_question('shortanswer', null, ['category' => $coursecatcat->id]);
// Add a quiz to the course category and put those questions into it.
$course = self::getDataGenerator()->create_course(['category' => $coursecategory->id]);
$quiz = $quizgenerator->create_instance(['course' => $course->id, 'grade' => 100.0, 'sumgrades' => 2, 'layout' => '1,0']);
quiz_add_quiz_question($question3->id, $quiz, 1);
// Create 2 nested categories with questions in them at course context level.
$course = self::getDataGenerator()->create_course();
$this->coursecontext = context_course::instance($course->id);
$courseparentcat1 = $this->create_question_category('Course Parent Cat', $this->coursecontext->id);
$coursechildcat1 = $this->create_question_category(
'Course Child Cat',
$this->coursecontext->id,
$courseparentcat1->id
);
$question4 = $questiongenerator->create_question('shortanswer', null, ['category' => $courseparentcat1->id]);
$question5 = $questiongenerator->create_question('shortanswer', null, ['category' => $coursechildcat1->id]);
// Make the questions 'in use'.
$quiz = $quizgenerator->create_instance(['course' => $course->id, 'grade' => 100.0, 'sumgrades' => 2, 'layout' => '1,0']);
quiz_add_quiz_question($question4->id, $quiz, 1);
quiz_add_quiz_question($question5->id, $quiz, 1);
// Create some nested categories with no questions in use.
$course = self::getDataGenerator()->create_course();
$context = context_course::instance($course->id);
$courseparentcat1 = $this->create_question_category('Stale Course Parent Cat1', $context->id);
$coursechildcat1 = $this->create_question_category('Stale Course Child Cat1', $context->id, $courseparentcat1->id);
$courseparentcat2 = $this->create_question_category('Stale Course Parent Cat2', $context->id);
$coursechildcat2 = $this->create_question_category('Stale Course Child Cat2', $context->id, $courseparentcat2->id);
$coursegrandchildcat1 = $this->create_question_category('Stale Course Grandchild Cat1', $context->id, $coursechildcat2->id);
$this->stalecoursecontext = context_course::instance($course->id);
// Make all the questions hidden.
$this->stalequestions[] = $questiongenerator->create_question('shortanswer',
null,
['category' => $courseparentcat1->id, 'status' => question_version_status::QUESTION_STATUS_HIDDEN]
);
$this->stalequestions[] = $questiongenerator->create_question('shortanswer',
null,
['category' => $coursechildcat1->id, 'status' => question_version_status::QUESTION_STATUS_HIDDEN]
);
$this->stalequestions[] = $questiongenerator->create_question('shortanswer',
null,
['category' => $courseparentcat2->id, 'status' => question_version_status::QUESTION_STATUS_HIDDEN]
);
$this->stalequestions[] = $questiongenerator->create_question('shortanswer',
null,
['category' => $coursechildcat2->id, 'status' => question_version_status::QUESTION_STATUS_HIDDEN]
);
$this->stalequestions[] = $questiongenerator->create_question('shortanswer',
null,
['category' => $coursegrandchildcat1->id, 'status' => question_version_status::QUESTION_STATUS_HIDDEN]
);
foreach ($this->stalequestions as $question) {
$DB->set_field('question_versions',
'status',
question_version_status::QUESTION_STATUS_HIDDEN,
['questionid' => $question->id]
);
}
// Set up a quiz with some categories and questions attached to it.
$course = self::getDataGenerator()->create_course();
$quiz = $quizgenerator->create_instance(['course' => $course->id, 'grade' => 100.0, 'sumgrades' => 2, 'layout' => '1,0']);
$this->quizcontext = context_module::instance($quiz->cmid);
$quizparentcat1 = $this->create_question_category('Quiz Mod Parent Cat1', $this->quizcontext->id);
$quizchildcat1 = $this->create_question_category('Quiz Mod Child Cat1', $this->quizcontext->id, $quizparentcat1->id);
$question1 = $questiongenerator->create_question('shortanswer', null, ['category' => $quizparentcat1->id]);
$question2 = $questiongenerator->create_question('shortanswer', null, ['category' => $quizchildcat1->id]);
quiz_add_quiz_question($question1->id, $quiz, 1);
quiz_add_quiz_question($question2->id, $quiz, 1);
}
/**
* Asserts that the pre-installation setup is correct.
*
* @return void
*/
public function test_setup_pre_install_data(): void {
global $DB;
$this->resetAfterTest();
$this->setup_pre_install_data();
$sitecontext = context_system::instance();
$allsitecats = $DB->get_records('question_categories', ['contextid' => $sitecontext->id], 'id ASC');
// Make sure we have 2 site level question categories below 'top' and that the child is below the parent.
$this->assertCount(3, $allsitecats);
$parentcat = next($allsitecats);
$childcat = end($allsitecats);
$this->assertEquals($parentcat->id, $childcat->parent);
// Make sure we have 1 question per the above site level question categories.
$questions = $this->get_question_data(array_map(static fn($cat) => $cat->id, $allsitecats));
usort($questions, static fn($a, $b) => $a->categoryid <=> $b->categoryid);
$this->assertCount(2, $questions);
$parentcatq = reset($questions);
$childcatq = end($questions);
$this->assertEquals($parentcat->id, $parentcatq->categoryid);
$this->assertEquals($childcat->id, $childcatq->categoryid);
// Make sure that the course category has a question category below 'top'.
$allcoursecatcats = $DB->get_records('question_categories', ['contextid' => $this->coursecatcontext->id], 'id ASC');
$this->assertCount(2, $allcoursecatcats);
$topcat = reset($allcoursecatcats);
$parentcat = end($allcoursecatcats);
$this->assertEquals($topcat->id, $parentcat->parent);
// Make sure we have 1 question in the above course category level question category.
$questions = $this->get_question_data(array_map(static fn($cat) => $cat->id, $allcoursecatcats));
$this->assertCount(1, $questions);
$question = reset($questions);
$this->assertEquals($parentcat->id, $question->categoryid);
// Make sure we have 3 question categories at course level (including 'top') with some questions in them.
$allcoursecats = $DB->get_records('question_categories', ['contextid' => $this->coursecontext->id], 'id ASC');
$this->assertCount(3, $allcoursecats);
$parentcat = next($allcoursecats);
$childcat = end($allcoursecats);
$this->assertEquals($parentcat->id, $childcat->parent);
$questions = $this->get_question_data(array_map(static fn($cat) => $cat->id, $allcoursecats));
$this->assertCount(2, $questions);
// Make sure we have 6 stale question categories at course level (including 'top') with some questions in them.
$questioncats = $DB->get_records('question_categories', ['contextid' => $this->stalecoursecontext->id], 'id ASC');
$this->assertCount(6, $questioncats);
$topcat = reset($questioncats);
$parentcat1 = next($questioncats);
$childcat1 = next($questioncats);
$parentcat2 = next($questioncats);
$childcat2 = next($questioncats);
$grandchildcat1 = next($questioncats);
$this->assertEquals($topcat->id, $parentcat1->parent);
$this->assertEquals($topcat->id, $parentcat2->parent);
$this->assertEquals($parentcat1->id, $childcat1->parent);
$this->assertEquals($parentcat2->id, $childcat2->parent);
$this->assertEquals($childcat2->id, $grandchildcat1->parent);
$questionids = $this->get_question_data(array_map(static fn($cat) => $cat->id, $questioncats));
$this->assertCount(5, $questionids);
}
/**
* Assert the installation task handles the deprecated contexts correctly.
*
* @return void
*/
public function test_qbank_install(): void {
global $DB;
$this->resetAfterTest();
$this->setup_pre_install_data();
$task = new \mod_qbank\task\transfer_question_categories();
$task->execute();
// Site context checks.
$sitecontext = context_system::instance();
$sitecontextcats = $DB->get_records('question_categories', ['contextid' => $sitecontext->id]);
// Should be no site context question categories left, not even 'top'.
$this->assertCount(0, $sitecontextcats);
$sitemodinfo = get_fast_modinfo(get_site());
$siteqbanks = $sitemodinfo->get_instances_of('qbank');
// We should have 1 new module on the site course.
$this->assertCount(1, $siteqbanks);
$siteqbank = reset($siteqbanks);
// Make doubly sure it got put into section 0 as these mod types are not rendered to the course page.
$this->assertEquals(0, $siteqbank->sectionnum);
// It should have our determined name.
$this->assertEquals('System shared question bank', $siteqbank->name);
$sitemodcontext = context_module::instance($siteqbank->get_course_module_record()->id);
// The 3 question categories including 'top' should now be at the new module context with their order intact.
$sitemodcats = $DB->get_records_select('question_categories',
'parent <> 0 AND contextid = :contextid',
['contextid' => $sitemodcontext->id],
'id ASC'
);
$this->assertCount(2, $sitemodcats);
$topcat = question_get_top_category($sitemodcontext->id);
$parentcat = reset($sitemodcats);
$childcat = next($sitemodcats);
$this->assertEquals($topcat->id, $parentcat->parent);
$this->assertEquals($parentcat->id, $childcat->parent);
// Course category context checks.
// Make sure that the course category has no question categories, not even 'top'.
$this->assertEquals(0, $DB->count_records('question_categories', ['contextid' => $this->coursecatcontext->id]));
$courses = $DB->get_records('course', ['category' => $this->coursecatcontext->instanceid], 'id ASC');
// We should have 2 courses in this category now, the original and the new one that holds our new mod instance.
$this->assertCount(2, $courses);
$newcourse = end($courses);
$coursecat = $DB->get_record('course_categories', ['id' => $newcourse->category]);
// Make sure the new course shortname is a unique name based on the category name and id.
$this->assertEquals("{$coursecat->name}-{$coursecat->id}", $newcourse->shortname);
// Make sure the new course fullname is based on the category name.
$this->assertEquals("Shared teaching resources for category: {$coursecat->name}", $newcourse->fullname);
$coursemodinfo = get_fast_modinfo($newcourse);
$coursecatqbanks = $coursemodinfo->get_instances_of('qbank');
// We should have 1 new module on this course.
$this->assertCount(1, $coursecatqbanks);
$coursecatqbank = reset($coursecatqbanks);
// Make sure the new module name is what we expect.
$this->assertEquals("{$coursecat->name} shared question bank", $coursecatqbank->name);
$coursecatqcats = $DB->get_records('question_categories', ['contextid' => $coursecatqbank->context->id], 'parent ASC');
// The 2 question categories should be moved to the module context now.
$this->assertCount(2, $coursecatqcats);
$topcat = reset($coursecatqcats);
$parentcat = end($coursecatqcats);
// Make sure the parent orders are correct.
$this->assertEquals($topcat->id, $parentcat->parent);
// Course context checks.
// Make sure that the course has no more question categories, not even 'top'.
$this->assertEquals(0, $DB->count_records('question_categories', ['contextid' => $this->coursecontext->id]));
$coursemodinfo = get_fast_modinfo($this->coursecontext->instanceid);
$course = $coursemodinfo->get_course();
$courseqbanks = $coursemodinfo->get_instances_of('qbank');
// We should have only 1 new mod instance in this course.
$this->assertCount(1, $coursecatqbanks);
// The module name should be what we expect.
$courseqbank = reset($courseqbanks);
$this->assertEquals("{$course->shortname} shared question bank", $courseqbank->name);
// Make sure the question categories still exist and that we have a new top one at the new module context.
$topcat = question_get_top_category($courseqbank->context->id);
$courseqcats = $DB->get_records_select('question_categories',
'parent <> 0 AND contextid = :contextid',
['contextid' => $courseqbank->context->id],
'id ASC'
);
$parentcat = reset($courseqcats);
$childcat = next($courseqcats);
$this->assertEquals($topcat->id, $parentcat->parent);
$this->assertEquals($parentcat->id, $childcat->parent);
// Stale course context checks.
// Make sure the stale course has no categories attached to it anymore and the questions were removed.
$this->assertFalse($DB->record_exists('question_categories', ['contextid' => $this->stalecoursecontext->id]));
foreach ($this->stalequestions as $stalequestion) {
$this->assertFalse($DB->record_exists('question', ['id' => $stalequestion->id]));
}
// Quiz module checks.
// Make sure the 3 categories at quiz context, including 'top' have not been touched.
$quizcategories = $DB->get_records('question_categories', ['contextid' => $this->quizcontext->id]);
$this->assertCount(3, $quizcategories);
$questions = $this->get_question_data(array_map(static fn($cat) => $cat->id, $quizcategories));
$this->assertCount(2, $questions);
}
}