diff --git a/backup/moodle2/restore_stepslib.php b/backup/moodle2/restore_stepslib.php index b971552d91c..23aa8a594df 100644 --- a/backup/moodle2/restore_stepslib.php +++ b/backup/moodle2/restore_stepslib.php @@ -5434,6 +5434,25 @@ class restore_move_module_questions_categories extends restore_execution_step { ]; $params += $categoryidparams; $DB->execute($sqlupdate, $params); + + // As explained in {@see restore_quiz_activity_structure_step::process_quiz_question_legacy_instance()} + // question_set_references relating to random questions restored from old backups, + // which pick from context_module question_categores, will have been restored with the wrong questioncontextid. + // So, now, we need to find those, and updated the questioncontextid. + // We can only find them by picking apart the filter conditions, and seeign which categories they refer to. + + // We need to check all the question_set_references belonging to this context_module. + $references = $DB->get_records('question_set_references', ['usingcontextid' => $newcontext->newitemid]); + foreach ($references as $reference) { + $filtercondition = json_decode($reference->filtercondition); + if (!empty($filtercondition->questioncategoryid) && + in_array($filtercondition->questioncategoryid, $categoryids)) { + // This is one of ours, update the questionscontextid. + $DB->set_field('question_set_references', + 'questionscontextid', $newcontext->newitemid, + ['id' => $reference->id]); + } + } } // Now set the parent id for the question categories that were in the top category in the course context @@ -6239,6 +6258,11 @@ trait restore_question_set_reference_data_trait { if ($context = $this->get_mappingid('context', $data->questionscontextid)) { $data->questionscontextid = $context; + } else { + $this->log('question_set_reference with old id ' . $data->id . + ' referenced question context ' . $data->questionscontextid . + ' which was not included in the backup. Therefore, this has been ' . + ' restored with the old questionscontextid.', backup::LOG_WARNING); } $filtercondition['cat'] = implode(',', [ diff --git a/mod/quiz/backup/moodle2/restore_quiz_stepslib.php b/mod/quiz/backup/moodle2/restore_quiz_stepslib.php index b7ff48de789..539f0dcc270 100644 --- a/mod/quiz/backup/moodle2/restore_quiz_stepslib.php +++ b/mod/quiz/backup/moodle2/restore_quiz_stepslib.php @@ -349,12 +349,17 @@ class restore_quiz_activity_structure_step extends restore_questions_activity_st if ($question->qtype === 'random') { // Set reference data. - $questionsetreference = new \stdClass(); + $questionsetreference = new stdClass(); $questionsetreference->usingcontextid = context_module::instance(get_coursemodule_from_instance( "quiz", $module->id, $module->course)->id)->id; $questionsetreference->component = 'mod_quiz'; $questionsetreference->questionarea = 'slot'; $questionsetreference->itemid = $data->id; + // If, in the orginal quiz that was backed up, this random question was pointing to a + // category in the quiz question bank, then (for reasons explained in {@see restore_move_module_questions_categories}) + // right now, $question->questioncontextid will incorrectly point to the course contextid. + // This will get fixed up later in restore_move_module_questions_categories + // as part of moving the question categories to the right place. $questionsetreference->questionscontextid = $question->questioncontextid; $filtercondition = new stdClass(); $filtercondition->questioncategoryid = $question->category; diff --git a/mod/quiz/tests/backup/restore_39_test.php b/mod/quiz/tests/backup/restore_39_test.php new file mode 100644 index 00000000000..91be76fa7c9 --- /dev/null +++ b/mod/quiz/tests/backup/restore_39_test.php @@ -0,0 +1,81 @@ +. + +namespace mod_quiz\backup; + +use advanced_testcase; +use backup; +use restore_controller; + +/** + * Test restoring 3.9 backups including random questions. + * + * @package mod_quiz + * @copyright 2024 Tomo Tsuyuki + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \restore_move_module_questions_categories + */ +final class restore_39_test extends advanced_testcase { + + public function test_restore_random_question_39(): void { + global $DB, $USER; + + $this->resetAfterTest(); + $this->setAdminUser(); + + // The example Moodle 3.9 backup file used in this test is an activity-level backup of a quiz. + // So, the backup contains just the quiz-level question bank which contains: + // | - Question category: Top + // | - Question category: Default for Test MDL-78902 quiz + // | - Question: Test MDL-78902 T/F question + // | - Question: Random (Default for Test MDL-78902 quiz) + // The quiz itself contains 1 question, the random question. + // So, during the restore, the quiz_slot needs to be updated to use a question_set_reference. + $backupfile = 'moodle_39_quiz_with_random_question_from_mod_context'; + + // Extract backup file. + $backupid = $backupfile; + $backuppath = make_backup_temp_directory($backupid); + get_file_packer('application/vnd.moodle.backup')->extract_to_pathname( + __DIR__ . "/../fixtures/$backupfile.mbz", $backuppath); + + // Restore the quiz activity in the backup from Moodle 3.9 to a new course. + $coursecat = self::getDataGenerator()->create_category(); + $course = self::getDataGenerator()->create_course(['category' => $coursecat->id]); + $rc = new restore_controller($backupid, $course->id, backup::INTERACTIVE_NO, + backup::MODE_GENERAL, $USER->id, backup::TARGET_EXISTING_ADDING); + $this->assertTrue($rc->execute_precheck()); + $rc->execute_plan(); + $rc->destroy(); + + // Get information about the quiz activity and confirm the references are correct. + $modinfo = get_fast_modinfo($course->id); + $quizzes = array_values($modinfo->get_instances_of('quiz')); + // Get contextid for the restored quiz activity. + $contextid = $quizzes[0]->context->id; + $qcats = $DB->get_records('question_categories', ['contextid' => $contextid], 'parent'); + // Confirm there are 2 question categories for the restored quiz activity. + $this->assertEquals(['top', 'Default for Test MDL-78902 quiz'], array_column($qcats, 'name')); + // Get question_set_references records for the restored quiz activity. + $references = $DB->get_records('question_set_references', ['usingcontextid' => $contextid]); + foreach ($references as $reference) { + $filtercondition = json_decode($reference->filtercondition); + // Confirm the questionscontextid is set correctly, which is from filter question category id. + $this->assertEquals($reference->questionscontextid, + $qcats[$filtercondition->questioncategoryid]->contextid); + } + } +} diff --git a/mod/quiz/tests/fixtures/moodle_39_quiz_with_random_question_from_mod_context.mbz b/mod/quiz/tests/fixtures/moodle_39_quiz_with_random_question_from_mod_context.mbz new file mode 100644 index 00000000000..ef2a668db90 Binary files /dev/null and b/mod/quiz/tests/fixtures/moodle_39_quiz_with_random_question_from_mod_context.mbz differ