MDL-73602 quiz: Restore random tag filters to new format

As we are now creating set references using the modern filtercondition
format, this ensures legacy tag filters are also converted directly to
the new format.

We no longer need the tag name as part of this, only the ID, so we have
factored the name out of `process_quiz_slot_tags`.
This commit is contained in:
Mark Johnson
2026-01-02 15:37:53 +00:00
parent 508cfc8619
commit 0b5311c2dc
5 changed files with 185 additions and 18 deletions
@@ -470,16 +470,25 @@ class restore_quiz_activity_structure_step extends restore_questions_activity_st
$data = (object) $data;
$slotid = $this->get_new_parentid('quiz_question_instance');
if ($this->task->is_samesite() && $tag = core_tag_tag::get($data->tagid, 'id, name')) {
$data->tagname = $tag->name;
} else if ($tag = core_tag_tag::get_by_name(0, $data->tagname, 'id, name')) {
$data->tagid = $tag->id;
} else {
$data->tagid = null;
$data->tagname = $tag->name;
$tagid = $data->tagid;
if (
!$this->task->is_samesite()
|| !core_tag_tag::get($data->tagid, 'id, name')
) {
// If we're on a different site, or the tag id doesn't exist anymore, look for a tag with the same name.
$tag = core_tag_tag::get_by_name(0, $data->tagname, 'id, name');
$tagid = $tag ? $tag->id : null;
}
if (is_null($tagid)) {
// There is no corresponding tag, so leave it out of the filter.
$this->log(
get_string('restorenotag', 'quiz', (object) ['tagname' => $data->tagname, 'slotid' => $slotid]),
\backup::LOG_WARNING,
);
return;
}
$tagstring = "{$data->tagid},{$data->tagname}";
$setreferencedata = $DB->get_record('question_set_references', [
'usingcontextid' => $this->task->get_contextid(),
'component' => 'mod_quiz',
@@ -487,7 +496,13 @@ class restore_quiz_activity_structure_step extends restore_questions_activity_st
'itemid' => $slotid,
]);
$filtercondition = json_decode($setreferencedata->filtercondition);
$filtercondition->tags[] = $tagstring;
if (!isset($filtercondition->filter->qtagids)) {
$filtercondition->filter->qtagids = (object) [
'jointype' => \qbank_tagquestion\tag_condition::JOINTYPE_DEFAULT,
'values' => [],
];
}
$filtercondition->filter->qtagids->values[] = $tagid;
$setreferencedata->filtercondition = json_encode($filtercondition);
$DB->update_record('question_set_references', $setreferencedata);
}
+1
View File
@@ -948,6 +948,7 @@ $string['requiresubnet'] = 'Require network address';
$string['requiresubnet_help'] = 'Quiz access may be restricted to particular subnets on the LAN or Internet by specifying a comma-separated list of partial or full IP address numbers. This can be useful for an invigilated (proctored) quiz, to ensure that only people in a certain location can access the quiz.';
$string['response'] = 'Response';
$string['responses'] = 'Responses';
$string['restorenotag'] = 'A tag matching \'{$a->tagname}\' could not be found when restoring the random question to quiz slot ID {$a->slotid}. The tag was removed from the question\'s filter conditions.';
$string['results'] = 'Results';
$string['returnattempt'] = 'Return to attempt';
$string['reuseifpossible'] = 'reuse previously removed';
Binary file not shown.
@@ -617,13 +617,166 @@ final class quiz_question_restore_test extends \advanced_testcase {
['itemid' => $slot->id, 'component' => 'mod_quiz', 'questionarea' => 'slot']);
$filterconditions = json_decode($setreference->filtercondition);
$tags = [];
foreach ($filterconditions->tags as $tagstring) {
$tag = explode(',', $tagstring);
$tags[] = $tag[1];
foreach ($filterconditions->filter->qtagids->values as $tagid) {
$tags[] = \core_tag_tag::get($tagid, 'id, name')->name;
}
$this->assertEquals([], array_diff($randomtags[$slot->slot], $tags));
}
}
/**
* Test pre 4.0 quiz restore for random question tags, where one of the tags in the random question condition is
* not in the backup but does already exist on the site.
*
* @covers \restore_quiz_activity_structure_step::process_quiz_question_legacy_instance
*/
public function test_pre_4_quiz_restore_for_random_question_tags_with_tag_not_in_backup(): void {
global $DB, $USER;
$this->resetAfterTest();
$collection = \core_tag_collection::create((object) ['name' => 'question']);
$missingtags = \core_tag_tag::create_if_missing($collection->id, ['name' => 'second question']);
$missingtag = reset($missingtags);
$randomtags = [
'1' => ['first question' => null, 'one' => null, 'number one' => null],
'2' => ['first question' => null, 'one' => null, 'number one' => null],
'3' => ['one' => null, 'number one' => null, 'second question' => $missingtag->id],
];
$backupid = 'abc';
$backuppath = make_backup_temp_directory($backupid);
get_file_packer('application/vnd.moodle.backup')
->extract_to_pathname(
__DIR__ . "/fixtures/moodle_311_quiz_missing_tag.mbz",
$backuppath,
);
// Do the restore to new course with default settings.
$categoryid = $DB->get_field_sql("SELECT MIN(id) FROM {course_categories}");
$newcourseid = \restore_dbops::create_new_course('Test fullname', 'Test shortname', $categoryid);
$rc = new \restore_controller(
$backupid,
$newcourseid,
\backup::INTERACTIVE_NO,
\backup::MODE_GENERAL,
$USER->id,
\backup::TARGET_NEW_COURSE,
);
$this->assertTrue($rc->execute_precheck());
$rc->execute_plan();
$rc->destroy();
// Get the information about the resulting course and check that it is set up correctly.
$modinfo = get_fast_modinfo($newcourseid);
$quiz = array_values($modinfo->get_instances_of('quiz'))[0];
$quizobj = \mod_quiz\quiz_settings::create($quiz->instance);
$structure = \mod_quiz\structure::create_for_quiz($quizobj);
// Count the questions in quiz qbank.
$context = \context_module::instance(get_coursemodule_from_instance("quiz", $quizobj->get_quizid(), $newcourseid)->id);
$this->assertEquals(2, $this->question_count($context->id));
// Are the correct slots returned?
$slots = $structure->get_slots();
$this->assertCount(3, $slots);
// Check if the tags match with the actual restored data.
foreach ($slots as $slot) {
$setreference = $DB->get_record(
'question_set_references',
['itemid' => $slot->id, 'component' => 'mod_quiz', 'questionarea' => 'slot'],
);
$filterconditions = json_decode($setreference->filtercondition);
$this->assertCount(count($randomtags[$slot->slot]), $filterconditions->filter->qtagids->values);
foreach ($filterconditions->filter->qtagids->values as $tagid) {
$name = \core_tag_tag::get($tagid, 'id, name')->name;
$this->assertTrue(array_key_exists($name, $randomtags[$slot->slot]));
// Check that tags in a filter, which were not otherwise present in the backup, were matched with the tag already
// on the site.
$id = $randomtags[$slot->slot][$name];
if (!is_null($id)) {
$this->assertEquals($id, $tagid);
}
}
}
}
/**
* Test pre 4.0 quiz restore for random question tags, where one of the tags in the random question condition is
* not in the backup or the target site.
*
* @covers \restore_quiz_activity_structure_step::process_quiz_question_legacy_instance
*/
public function test_pre_4_quiz_restore_for_random_question_tags_with_tag_not_in_backup_or_site(): void {
global $DB, $USER;
$this->resetAfterTest();
$randomtags = [
'1' => ['first question', 'one', 'number one'],
'2' => ['first question', 'one', 'number one'],
'3' => ['one', 'number one'],
];
$backupid = 'abc';
$backuppath = make_backup_temp_directory($backupid);
get_file_packer('application/vnd.moodle.backup')
->extract_to_pathname(
__DIR__ . "/fixtures/moodle_311_quiz_missing_tag.mbz",
$backuppath,
);
// Do the restore to new course with default settings.
$categoryid = $DB->get_field_sql("SELECT MIN(id) FROM {course_categories}");
$newcourseid = \restore_dbops::create_new_course('Test fullname', 'Test shortname', $categoryid);
$rc = new \restore_controller(
$backupid,
$newcourseid,
\backup::INTERACTIVE_NO,
\backup::MODE_GENERAL,
$USER->id,
\backup::TARGET_NEW_COURSE,
);
$this->assertTrue($rc->execute_precheck());
$rc->execute_plan();
$rc->destroy();
// Get the information about the resulting course and check that it is set up correctly.
$modinfo = get_fast_modinfo($newcourseid);
$quiz = array_values($modinfo->get_instances_of('quiz'))[0];
$quizobj = \mod_quiz\quiz_settings::create($quiz->instance);
$structure = \mod_quiz\structure::create_for_quiz($quizobj);
// Count the questions in quiz qbank.
$context = \context_module::instance(get_coursemodule_from_instance("quiz", $quizobj->get_quizid(), $newcourseid)->id);
$this->assertEquals(2, $this->question_count($context->id));
// Are the correct slots returned?
$slots = $structure->get_slots();
$this->assertCount(3, $slots);
// Check if the tags match with the actual restored data.
foreach ($slots as $slot) {
$setreference = $DB->get_record(
'question_set_references',
['itemid' => $slot->id, 'component' => 'mod_quiz', 'questionarea' => 'slot'],
);
$filterconditions = json_decode($setreference->filtercondition);
$tags = [];
foreach ($filterconditions->filter->qtagids->values as $tagid) {
$tags[] = \core_tag_tag::get($tagid, 'id, name')->name;
}
$this->assertEquals([], array_diff($randomtags[$slot->slot], $tags));
}
$log = $DB->get_fieldset(
'backup_logs',
'message',
['backupid' => $rc->get_restoreid(), 'loglevel' => \backup::LOG_WARNING],
);
$slotid = end($slots)->id;
$this->assertContains(
"A tag matching 'second question' could not be found when restoring the random question to quiz slot "
. "ID {$slotid}. The tag was removed from the question's filter conditions.",
$log,
);
}
/**
+4 -6
View File
@@ -88,9 +88,7 @@ final class tags_test extends \advanced_testcase {
$this->assertNotFalse($tag3);
$slottags = $this->get_tags_for_slot($question->slotid);
$slottags = reset($slottags);
$slottags = explode(',', $slottags);
$this->assertEquals("{$tag2->id},{$tag2->name}", "{$slottags[0]},{$slottags[1]}");
$this->assertEquals($tag2->id, $slottags[0]);
// Course context question cats get restored to a default qbank module instance.
$modinfo = get_fast_modinfo($newcourseid);
@@ -103,7 +101,7 @@ final class tags_test extends \advanced_testcase {
$randomincludingsubcategories = $DB->get_record('question_set_references',
['itemid' => reset($slots)->id, 'component' => 'mod_quiz', 'questionarea' => 'slot']);
$filtercondition = json_decode($randomincludingsubcategories->filtercondition);
$this->assertEquals(0, $filtercondition->includingsubcategories);
$this->assertEquals(0, $filtercondition->filter->category->filteroptions->includesubcategories);
}
/**
@@ -118,8 +116,8 @@ final class tags_test extends \advanced_testcase {
['itemid' => $slotid, 'component' => 'mod_quiz', 'questionarea' => 'slot']);
if (isset($referencedata->filtercondition)) {
$filtercondition = json_decode($referencedata->filtercondition);
if (isset($filtercondition->tags)) {
return $filtercondition->tags;
if (isset($filtercondition->filter->qtagids)) {
return $filtercondition->filter->qtagids->values;
}
}
return [];