From f9cdb2ff7ccfb0bbea08550410fb64594be2b60d Mon Sep 17 00:00:00 2001 From: Andrew Hancox Date: Thu, 25 Sep 2025 15:07:01 +0100 Subject: [PATCH] MDL-86417 quiz: Fix for legacy question_set_references on backup --- public/backup/moodle2/backup_stepslib.php | 2 + .../backup/backup_question_selection_test.php | 77 +++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/public/backup/moodle2/backup_stepslib.php b/public/backup/moodle2/backup_stepslib.php index db0e9c35bc4..400a45eca5e 100644 --- a/public/backup/moodle2/backup_stepslib.php +++ b/public/backup/moodle2/backup_stepslib.php @@ -28,6 +28,7 @@ defined('MOODLE_INTERNAL') || die(); use core_question\local\bank\random_question_loader; +use core_question\question_reference_manager; /** * Create the temp dir where backup/restore will happen and create temp ids table. @@ -308,6 +309,7 @@ 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)); } if (empty($setreferencequestionids)) { diff --git a/public/mod/quiz/tests/backup/backup_question_selection_test.php b/public/mod/quiz/tests/backup/backup_question_selection_test.php index 1a2a9c31e4f..3de162fa529 100644 --- a/public/mod/quiz/tests/backup/backup_question_selection_test.php +++ b/public/mod/quiz/tests/backup/backup_question_selection_test.php @@ -211,6 +211,83 @@ final class backup_question_selection_test extends \advanced_testcase { $rc->destroy(); } + /** + * Test that backing up a quiz only includes the questions owned or used by the quiz + * when the quiz has legacy JSON values in the question_set_references table. + */ + public function test_quiz_backup_with_legacy_reference_filter_condition(): void { + global $DB; + $this->resetAfterTest(); + + [ + $manager, + $quiz, + $quizquestions, + $coursequestions, + $sharedquestions, + ] = $this->create_quiz_and_questions(); + + // Revert the filtercondition to the legacy JSON format. + $questionsetreference = $DB->get_record('question_set_references', []); + $filtercondition = json_decode($questionsetreference->filtercondition); + $tag = \core_tag_tag::get($filtercondition->filter->qtagids->values[0]); + $questionsetreference->filtercondition = json_encode([ + 'questioncategoryid' => $filtercondition->filter->category->values[0], + 'includingsubcategories' => $filtercondition->filter->category->filteroptions->includesubcategories, + 'tags' => ["$tag->id,$tag->name"], + ]); + $DB->update_record('question_set_references', $questionsetreference); + + // Backup the quiz. + $bc = new \backup_controller( + \backup::TYPE_1ACTIVITY, + $quiz->cmid, + \backup::FORMAT_MOODLE, + \backup::INTERACTIVE_NO, + \backup::MODE_IMPORT, + $manager->id, + ); + $backupid = $bc->get_backupid(); + $bc->execute_plan(); + $bc->destroy(); + + $course2 = $this->getDataGenerator()->create_course(); + $this->getDataGenerator()->enrol_user($manager->id, $course2->id, 'manager'); + $rc = new \restore_controller( + $backupid, + $course2->id, + \backup::INTERACTIVE_NO, + \backup::MODE_IMPORT, + $manager->id, + \backup::TARGET_CURRENT_ADDING + ); + $rc->execute_precheck(); + $backupquestions = $DB->get_records_menu('backup_ids_temp', ['itemname' => 'question'], '', 'id, itemid'); + // Backup should contain used questions from shared qbanks. + $this->assertContains((string) $sharedquestions['sharedparentcat']['sharedchildcat']['sharedq3']->id, $backupquestions); + $this->assertContains((string) $coursequestions['courseparentcat']['courseq2']->id, $backupquestions); + // Backup should contain all questions from quiz's bank. + $this->assertContains((string) $quizquestions['quizparentcat']['quizq1']->id, $backupquestions); + $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); + // 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) $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); + // Clean up. + $rc->execute_plan(); + $rc->destroy(); + } + /** * Test that backing up a quiz only includes the questions used in or belonging to the course. *