Merge branch 'MDL-85869-M500' of https://github.com/ziegenberg/moodle into MOODLE_500_STABLE

This commit is contained in:
Mihail Geshoski
2025-09-04 23:55:07 +08:00
2 changed files with 117 additions and 3 deletions
@@ -208,9 +208,31 @@ class transfer_question_categories extends adhoc_task {
global $DB;
$movedcategories = [];
$subcatids = $DB->get_fieldset('question_categories', 'id', ['parent' => $categoryid]);
foreach ($subcatids as $subcatid) {
$DB->set_field('question_categories', 'contextid', $newcontext->id, ['id' => $subcatid]);
$subcatids = $DB->get_records('question_categories', ['parent' => $categoryid]);
foreach ($subcatids as $subcatid => $data) {
// Because of the fallback above, where categories pointing to a
// missing contextid are all moved to the new shared system-level
// question bank, some categories are moved from previously
// separate contextids to the same context. This can violate
// unique indexes, so we fix this by ensuring uniqueness.
// For the stamp, we just generate a new stamp if required.
if ($DB->record_exists('question_categories', ['stamp' => $data->stamp, 'contextid' => $newcontext->id])) {
$data->stamp = make_unique_id_code();
}
// The idnumber we just reset duplicates to null, as is done in other places.
if (
$data->idnumber !== null &&
$DB->record_exists('question_categories', ['idnumber' => $data->idnumber, 'contextid' => $newcontext->id])
) {
$data->idnumber = null;
}
// Update the contextid and save the category.
$data->contextid = $newcontext->id;
$DB->update_record('question_categories', $data);
$movedcategories[] = $subcatid;
$movedcategories = array_merge(
$this->move_subcategories_to_context($subcatid, $newcontext),
@@ -637,6 +637,98 @@ final class transfer_question_categories_test extends \advanced_testcase {
);
}
/**
* Assert the installation task handles the missing contexts correctly.
*
* @return void
*/
public function test_qbank_install_with_missing_context(): void {
global $DB;
$this->resetAfterTest();
self::setAdminUser();
$questiongenerator = self::getDataGenerator()->get_plugin_generator('core_question');
// The problem is that question categories that used to related to contextids
// which no longer exist are now all moved to the new system-level shared
// question bank. This moving categories together can cause unique key violations.
// Create 2 orphaned categories where the contextid no longer exists, with the same stamp and idnumber.
// We need to do this by creating in a real context, then deleting the context,
// because create category logs, which needs a valid context id.
$tamperedstamp = make_unique_id_code();
$context1 = context_course::instance(self::getDataGenerator()->create_course()->id);
$oldcat1 = $this->create_question_category('Lost category 1', $context1->id);
$oldcat1->stamp = $tamperedstamp;
$oldcat1->idnumber = 'tamperedidnumber';
$DB->update_record('question_categories', $oldcat1);
$DB->delete_records('context', ['id' => $context1->id]);
$context2 = context_course::instance(self::getDataGenerator()->create_course()->id);
$oldcat2 = $this->create_question_category('Lost category 2', $context2->id);
$oldcat2->stamp = $tamperedstamp;
$oldcat2->idnumber = 'tamperedidnumber';
$DB->update_record('question_categories', $oldcat2);
$DB->delete_records('context', ['id' => $context2->id]);
// Add a question to each category.
$question1 = $questiongenerator->create_question('shortanswer', null, ['category' => $oldcat1->id]);
$question2 = $questiongenerator->create_question('shortanswer', null, ['category' => $oldcat2->id]);
// Make the questions 'in use'.
$quizcourse = self::getDataGenerator()->create_course();
$quiz = self::getDataGenerator()->get_plugin_generator('mod_quiz')->create_instance(
['course' => $quizcourse->id, 'grade' => 100.0, 'sumgrades' => 2, 'layout' => '1,0']
);
quiz_add_quiz_question($question1->id, $quiz);
quiz_add_quiz_question($question2->id, $quiz);
// Make sure the caches are reset so that the contexts are not cached.
\core\context_helper::reset_caches();
// Run the task.
$task = new transfer_question_categories();
$task->execute();
// An important thing to verify is that the task completes without errors,
// for example unique key violations.
// Verify - there should be a single question bank in the site course with the expected name.
$sitemodinfo = get_fast_modinfo(get_site());
$siteqbanks = $sitemodinfo->get_instances_of('qbank');
$this->assertCount(1, $siteqbanks);
$siteqbank = reset($siteqbanks);
$this->assertEquals('System shared question bank', $siteqbank->name);
// The two previously orphaned categories should now be in this site questions bank, with a top category.
$sitemodcontext = context_module::instance($siteqbank->get_course_module_record()->id);
$sitemodcats = $DB->get_records_select(
'question_categories',
'parent <> 0 AND contextid = :contextid',
['contextid' => $sitemodcontext->id],
'id ASC',
);
// Work out which category is which.
$movedcat1 = null;
$movedcat2 = null;
foreach ($sitemodcats as $movedcat) {
if ($movedcat->name === $oldcat1->name) {
$movedcat1 = $movedcat;
}
if ($movedcat->name === $oldcat2->name) {
$movedcat2 = $movedcat;
}
}
$this->assertNotNull($movedcat1);
$this->assertNotNull($movedcat2);
// Verify the properties of the moved categories.
$this->assertNotEquals($movedcat1->stamp, $movedcat2->stamp);
$this->assertNotEquals($movedcat1->idnumber, $movedcat2->idnumber);
$this->assertEquals(question_get_top_category($sitemodcontext->id)->id, $movedcat1->parent);
$this->assertEquals(question_get_top_category($sitemodcontext->id)->id, $movedcat2->parent);
}
public function test_fix_wrong_parents(): void {
$this->resetAfterTest();
$this->setup_pre_install_data();