MDL-85721 qtype_multichoice: Generate default options on restore

qtype_multichoice::get_question_options() will create a default options
object if no qtype_multichoice_options record exists. This means if we
restore a backup containing a multichoice question without any options
(see previous commit), it will always create a duplicate.

This change generates a default set of options for the backupdata if
none exists, so the identity hash will match if the same question exists
in the database without options, and we dont get duplicates.
This commit is contained in:
Mark Johnson
2025-07-01 11:54:50 +01:00
parent 7c0600ae16
commit d3d2d67c8e
3 changed files with 102 additions and 1 deletions
@@ -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;
}
}
+1 -1
View File
@@ -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;
@@ -0,0 +1,86 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
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 <[email protected]>
* @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'));
}
}