diff --git a/question/type/multichoice/backup/moodle2/restore_qtype_multichoice_plugin.class.php b/question/type/multichoice/backup/moodle2/restore_qtype_multichoice_plugin.class.php index 7ef6ab68ec7..71120c4fec0 100644 --- a/question/type/multichoice/backup/moodle2/restore_qtype_multichoice_plugin.class.php +++ b/question/type/multichoice/backup/moodle2/restore_qtype_multichoice_plugin.class.php @@ -160,4 +160,19 @@ class restore_qtype_multichoice_plugin extends restore_qtype_plugin { return $contents; } + + #[\Override] + public static function convert_backup_to_questiondata(array $backupdata): \stdClass { + global $CFG; + require_once($CFG->dirroot . '/question/type/multichoice/questiontype.php'); + $questiondata = parent::convert_backup_to_questiondata($backupdata); + if (count(get_object_vars($questiondata->options)) <= 1) { + // Historically, old versions of multichoice subquestions had their options record deleted. + // As qtype_multichoice::get_question_options() sets default options in this case, we need + // to do the same here. See MDL-85721. + $defaultoptions = (new qtype_multichoice())->create_default_options($questiondata); + $questiondata->options = (object) array_merge((array) $questiondata->options, (array) $defaultoptions); + } + return $questiondata; + } } diff --git a/question/type/multichoice/questiontype.php b/question/type/multichoice/questiontype.php index b1d1b4ff0cf..59627b2d73a 100644 --- a/question/type/multichoice/questiontype.php +++ b/question/type/multichoice/questiontype.php @@ -88,7 +88,7 @@ class qtype_multichoice extends question_type { * @param object $question The queston we are working with. * @return object The options object. */ - protected function create_default_options($question) { + public function create_default_options($question) { // Create a default question options record. $options = new stdClass(); $options->questionid = $question->id; diff --git a/question/type/multichoice/tests/restore_test.php b/question/type/multichoice/tests/restore_test.php new file mode 100644 index 00000000000..7c981323a25 --- /dev/null +++ b/question/type/multichoice/tests/restore_test.php @@ -0,0 +1,86 @@ +. + +namespace qtype_multichoice; + +/** + * Unit tests for restore_qtype_multichoice_plugin + * + * @package qtype_multichoice + * @copyright 2025 onwards Catalyst IT EU {@link https://catalyst-eu.net} + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \restore_qtype_multichoice_plugin + */ +final class restore_test extends \advanced_testcase { + /** + * Duplicate a quiz containing a multichoice question with no options record. + */ + public function test_restore_quiz_with_edited_questions(): void { + global $CFG, $DB, $USER; + require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); + require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); + $this->resetAfterTest(); + $this->setAdminUser(); + + // Create a course and a user with editing teacher capabilities. + $generator = $this->getDataGenerator(); + $course1 = $generator->create_course(); + $context = \context_course::instance($course1->id); + $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); + $initialcount = $DB->count_records('question'); + + // Create a question category. + $cat = $questiongenerator->create_question_category(['contextid' => $context->id]); + + // Create a quiz containing a multichoice question from the qbank. + $quiz = $this->getDataGenerator()->get_plugin_generator('mod_quiz')->create_instance(['course' => $course1->id]); + $question = $questiongenerator->create_question('multichoice', 'one_of_four', ['category' => $cat->id]); + quiz_add_quiz_question($question->id, $quiz); + + // Delete the multichoice_options record. + $DB->delete_records('qtype_multichoice_options', ['questionid' => $question->id]); + + // Confirm we have created 1 additional question. + $this->assertEquals($initialcount + 1, $DB->count_records('question')); + + // Backup quiz. + $bc = new \backup_controller(\backup::TYPE_1ACTIVITY, $quiz->cmid, \backup::FORMAT_MOODLE, + \backup::INTERACTIVE_NO, \backup::MODE_IMPORT, $USER->id); + $backupid = $bc->get_backupid(); + $bc->execute_plan(); + $bc->destroy(); + + // Restore the backup into the same course. + $rc = new \restore_controller($backupid, $course1->id, \backup::INTERACTIVE_NO, \backup::MODE_IMPORT, + $USER->id, \backup::TARGET_CURRENT_ADDING); + $rc->execute_precheck(); + $rc->execute_plan(); + $rc->destroy(); + $this->assertDebuggingCalled(); + + // Both quizzes should refer to the same original question. + $quizzes = get_fast_modinfo($course1->id)->get_instances_of('quiz'); + $this->assertCount(2, $quizzes); + foreach ($quizzes as $quiz) { + $structure = \mod_quiz\question\bank\qbank_helper::get_question_structure($quiz->instance, $quiz->context); + $this->assertEquals($structure[1]->questionid, $question->id); + } + + // There should be no additional questions created during the restore. + $this->assertEquals($initialcount + 1, $DB->count_records('question')); + } +}