MDL-87004 backup: Ensure backups include all random questions

Backing up quizzes containing random questions from multiple different
categories was only including one of the categories in the backup.

This was due to doing $array + $array instead of array_merge(), meaning
items in the second array with keys that were already present in the
first did not get added to the result.
This commit is contained in:
Mark Johnson
2025-10-24 09:45:31 +01:00
parent 1beae326aa
commit da518313d6
2 changed files with 53 additions and 22 deletions
+4 -1
View File
@@ -310,7 +310,10 @@ trait backup_question_set_reference_trait {
foreach ($setreferenceconditions as $setreferencecondition) {
$conditions = json_decode($setreferencecondition, true);
$conditions = question_reference_manager::convert_legacy_set_reference_filter_condition($conditions);
$setreferencequestionids += array_keys($randomloader->get_filtered_questions($conditions['filter'], 0));
$setreferencequestionids = array_merge(
$setreferencequestionids,
array_keys($randomloader->get_filtered_questions($conditions['filter'], 0)),
);
}
if (empty($setreferencequestionids)) {
return;
@@ -90,11 +90,16 @@ final class backup_question_selection_test extends \advanced_testcase {
'sharedq4' => 'shortanswer',
],
],
'tagcat' => [
'tagcat1' => [
'tagq1' => 'shortanswer',
'tagq2' => 'shortanswer',
'tagq3' => 'shortanswer',
],
'tagcat2' => [
'tagq4' => 'shortanswer',
'tagq5' => 'shortanswer',
'tagq6' => 'shortanswer',
],
]
);
$quiz = $this->create_test_quiz($course);
@@ -113,10 +118,11 @@ final class backup_question_selection_test extends \advanced_testcase {
]
);
$questiongenerator->create_question_tag(['questionid' => $sharedquestions['tagcat']['tagq1']->id, 'tag' => 'mytag']);
$questiongenerator->create_question_tag(['questionid' => $sharedquestions['tagcat']['tagq2']->id, 'tag' => 'mytag']);
$questiongenerator->create_question_tag(['questionid' => $sharedquestions['tagcat1']['tagq1']->id, 'tag' => 'mytag']);
$questiongenerator->create_question_tag(['questionid' => $sharedquestions['tagcat1']['tagq2']->id, 'tag' => 'mytag']);
$questiongenerator->create_question_tag(['questionid' => $sharedquestions['tagcat2']['tagq5']->id, 'tag' => 'mytag']);
$tags = \core_tag_tag::get_item_tags('core_question', 'question', $sharedquestions['tagcat']['tagq1']->id);
$tags = \core_tag_tag::get_item_tags('core_question', 'question', $sharedquestions['tagcat1']['tagq1']->id);
$mytag = reset($tags);
// Add a question from the shared bank child category.
@@ -125,14 +131,27 @@ final class backup_question_selection_test extends \advanced_testcase {
quiz_add_quiz_question($coursequestions['courseparentcat']['courseq2']->id, $quiz);
// Add a question from the quiz bank categories.
quiz_add_quiz_question($quizquestions['quizparentcat']['quizq1']->id, $quiz);
// Add a random question to select tagged questions.
// Add random question to select tagged questions from 2 different categories.
$settings = quiz_settings::create($quiz->id);
$structure = structure::create_for_quiz($settings);
$structure->add_random_questions(1, 1, [
'filter' => [
'category' => [
'jointype' => \core\output\datafilter::JOINTYPE_ANY,
'values' => [$sharedquestions['tagcat']['tagq1']->category],
'values' => [$sharedquestions['tagcat1']['tagq1']->category],
'filteroptions' => ['includesubcategories' => false],
],
'qtagids' => [
'jointype' => \core\output\datafilter::JOINTYPE_ANY,
'values' => [$mytag->id],
],
],
]);
$structure->add_random_questions(1, 1, [
'filter' => [
'category' => [
'jointype' => \core\output\datafilter::JOINTYPE_ANY,
'values' => [$sharedquestions['tagcat2']['tagq4']->category],
'filteroptions' => ['includesubcategories' => false],
],
'qtagids' => [
@@ -194,9 +213,10 @@ final class backup_question_selection_test extends \advanced_testcase {
$this->assertContains((string) $quizquestions['quizparentcat']['quizq2']->id, $backupquestions);
$this->assertContains((string) $quizquestions['quizparentcat']['quizchildcat']['quizq3']->id, $backupquestions);
$this->assertContains((string) $quizquestions['quizparentcat']['quizchildcat']['quizq4']->id, $backupquestions);
// Backup should contain questions matched by random question filter.
$this->assertContains((string) $sharedquestions['tagcat']['tagq1']->id, $backupquestions);
$this->assertContains((string) $sharedquestions['tagcat']['tagq2']->id, $backupquestions);
// Backup should contain questions matched by random question filters.
$this->assertContains((string) $sharedquestions['tagcat1']['tagq1']->id, $backupquestions);
$this->assertContains((string) $sharedquestions['tagcat1']['tagq2']->id, $backupquestions);
$this->assertContains((string) $sharedquestions['tagcat2']['tagq5']->id, $backupquestions);
// All other questions should be excluded.
$this->assertNotContains((string) $sharedquestions['sharedparentcat']['sharedq1']->id, $backupquestions);
$this->assertNotContains((string) $sharedquestions['sharedparentcat']['sharedq2']->id, $backupquestions);
@@ -204,8 +224,10 @@ final class backup_question_selection_test extends \advanced_testcase {
$this->assertNotContains((string) $coursequestions['courseparentcat']['courseq1']->id, $backupquestions);
$this->assertNotContains((string) $coursequestions['courseparentcat']['coursechildcat']['courseq3']->id, $backupquestions);
$this->assertNotContains((string) $coursequestions['courseparentcat']['coursechildcat']['courseq4']->id, $backupquestions);
$this->assertNotContains((string) $sharedquestions['tagcat']['tagq3']->id, $backupquestions);
$this->assertCount(8, $backupquestions);
$this->assertNotContains((string) $sharedquestions['tagcat1']['tagq3']->id, $backupquestions);
$this->assertNotContains((string) $sharedquestions['tagcat2']['tagq4']->id, $backupquestions);
$this->assertNotContains((string) $sharedquestions['tagcat2']['tagq6']->id, $backupquestions);
$this->assertCount(9, $backupquestions);
// Clean up.
$rc->execute_plan();
$rc->destroy();
@@ -271,9 +293,10 @@ final class backup_question_selection_test extends \advanced_testcase {
$this->assertContains((string) $quizquestions['quizparentcat']['quizq2']->id, $backupquestions);
$this->assertContains((string) $quizquestions['quizparentcat']['quizchildcat']['quizq3']->id, $backupquestions);
$this->assertContains((string) $quizquestions['quizparentcat']['quizchildcat']['quizq4']->id, $backupquestions);
// Backup should contain questions matched by random question filter.
$this->assertContains((string) $sharedquestions['tagcat']['tagq1']->id, $backupquestions);
$this->assertContains((string) $sharedquestions['tagcat']['tagq2']->id, $backupquestions);
// Backup should contain questions matched by random question filters.
$this->assertContains((string) $sharedquestions['tagcat1']['tagq1']->id, $backupquestions);
$this->assertContains((string) $sharedquestions['tagcat1']['tagq2']->id, $backupquestions);
$this->assertContains((string) $sharedquestions['tagcat2']['tagq5']->id, $backupquestions);
// All other questions should be excluded.
$this->assertNotContains((string) $sharedquestions['sharedparentcat']['sharedq1']->id, $backupquestions);
$this->assertNotContains((string) $sharedquestions['sharedparentcat']['sharedq2']->id, $backupquestions);
@@ -281,8 +304,10 @@ final class backup_question_selection_test extends \advanced_testcase {
$this->assertNotContains((string) $coursequestions['courseparentcat']['courseq1']->id, $backupquestions);
$this->assertNotContains((string) $coursequestions['courseparentcat']['coursechildcat']['courseq3']->id, $backupquestions);
$this->assertNotContains((string) $coursequestions['courseparentcat']['coursechildcat']['courseq4']->id, $backupquestions);
$this->assertNotContains((string) $sharedquestions['tagcat']['tagq3']->id, $backupquestions);
$this->assertCount(8, $backupquestions);
$this->assertNotContains((string) $sharedquestions['tagcat1']['tagq3']->id, $backupquestions);
$this->assertNotContains((string) $sharedquestions['tagcat2']['tagq4']->id, $backupquestions);
$this->assertNotContains((string) $sharedquestions['tagcat2']['tagq6']->id, $backupquestions);
$this->assertCount(9, $backupquestions);
// Clean up.
$rc->execute_plan();
$rc->destroy();
@@ -338,15 +363,18 @@ final class backup_question_selection_test extends \advanced_testcase {
$this->assertContains((string) $quizquestions['quizparentcat']['quizq2']->id, $backupquestions);
$this->assertContains((string) $quizquestions['quizparentcat']['quizchildcat']['quizq3']->id, $backupquestions);
$this->assertContains((string) $quizquestions['quizparentcat']['quizchildcat']['quizq4']->id, $backupquestions);
// Backup should contain questions matched by random question filter.
$this->assertContains((string) $sharedquestions['tagcat']['tagq1']->id, $backupquestions);
$this->assertContains((string) $sharedquestions['tagcat']['tagq2']->id, $backupquestions);
// Backup should contain questions matched by random question filters.
$this->assertContains((string) $sharedquestions['tagcat1']['tagq1']->id, $backupquestions);
$this->assertContains((string) $sharedquestions['tagcat1']['tagq2']->id, $backupquestions);
$this->assertContains((string) $sharedquestions['tagcat2']['tagq5']->id, $backupquestions);
// All other questions should be excluded.
$this->assertNotContains((string) $sharedquestions['sharedparentcat']['sharedq1']->id, $backupquestions);
$this->assertNotContains((string) $sharedquestions['sharedparentcat']['sharedq2']->id, $backupquestions);
$this->assertNotContains((string) $sharedquestions['sharedparentcat']['sharedchildcat']['sharedq4']->id, $backupquestions);
$this->assertNotContains((string) $sharedquestions['tagcat']['tagq3']->id, $backupquestions);
$this->assertCount(11, $backupquestions);
$this->assertNotContains((string) $sharedquestions['tagcat1']['tagq3']->id, $backupquestions);
$this->assertNotContains((string) $sharedquestions['tagcat2']['tagq4']->id, $backupquestions);
$this->assertNotContains((string) $sharedquestions['tagcat2']['tagq6']->id, $backupquestions);
$this->assertCount(12, $backupquestions);
// Clean up.
$rc->execute_plan();
$rc->destroy();