Merge branch 'MDL-86136_500_STABLE' of https://github.com/marxjohnson/moodle into MOODLE_500_STABLE

This commit is contained in:
Jun Pataleta
2026-01-22 08:32:34 +08:00
7 changed files with 719 additions and 29 deletions
+45 -3
View File
@@ -464,6 +464,39 @@ abstract class restore_dbops {
$xmlparser->process();
}
/**
* Store ids associated with any activity in the backup that supports FEATURE_PUBLISHES_QUESTIONS.
*
* @param string $restoreid The restore ID.
* @param string $activitiespath The path to the `activities` folder in the backup being restored.
*/
public static function load_questionbanks_to_tempids(string $restoreid, string $activitiespath): void {
if (!is_dir($activitiespath)) {
return;
}
// Get modules that publish questions.
$qmodules = array_filter(
array_keys(core\component::get_all_plugins_list('mod')),
fn($module) => plugin_supports('mod', $module, FEATURE_PUBLISHES_QUESTIONS),
);
foreach (scandir($activitiespath) as $activitydir) {
[$modname] = explode('_', $activitydir);
if (!in_array($modname, $qmodules)) {
continue;
}
$activityfile = "{$activitiespath}/{$activitydir}/{$modname}.xml";
if (!file_exists($activityfile)) { // Shouldn't happen ever, but...
throw new backup_helper_exception('missing_moodle_backup_xml_file', $activityfile);
}
// Parse each activity's file, storing the relevant data in the database.
$xmlparser = new progressive_parser();
$xmlparser->set_file($activityfile);
$xmlprocessor = new restore_questionbanks_parser_processor($restoreid);
$xmlparser->set_processor($xmlprocessor);
$xmlparser->process();
}
}
/**
* Check all the included categories and questions, deciding the action to perform
* for each one (mapping / creation) and returning one array of problems in case
@@ -609,9 +642,18 @@ abstract class restore_dbops {
$topcats = 0;
// get categories in context (bank)
$categories = self::restore_get_question_categories($restoreid, $contextid, $contextlevel);
// cache permissions if $targetcontext is found
if ($targetcontext = self::restore_find_best_target_context($categories, $courseid, $contextlevel)) {
if (
$contextlevel == \core\context\module::LEVEL
&& self::get_backup_ids_record($restoreid, 'questionbank', $contextid)
) {
// Don't look for an existing module context, we have the original context in the backup,
// so we'll put the categories in the course context for now and move them once the activity is restored.
$targetcontext = core\context\course::instance($courseid);
} else {
$targetcontext = self::restore_find_best_target_context($categories, $courseid, $contextlevel);
}
if ($targetcontext) {
// Cache permissions if $targetcontext is found.
$canmanagecategory = has_capability('moodle/question:managecategory', $targetcontext, $userid);
$canadd = has_capability('moodle/question:add', $targetcontext, $userid);
}
@@ -173,6 +173,7 @@ abstract class restore_prechecks_helper {
$progress->progress($majorstep++);
// Check we are able to restore and the categories and questions
restore_dbops::load_questionbanks_to_tempids($restoreid, $controller->get_plan()->get_basepath() . '/activities');
$file = $controller->get_plan()->get_basepath() . '/questions.xml';
restore_dbops::load_categories_and_questions_to_tempids($restoreid, $file);
if ($problems = restore_dbops::precheck_categories_and_questions($restoreid, $courseid, $userid, $samesite)) {
@@ -0,0 +1,59 @@
<?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/>.
defined('MOODLE_INTERNAL' || die());
require_once($CFG->dirroot . '/backup/util/xml/parser/processors/grouped_parser_processor.class.php');
/**
* Parse and store activity data for activities that publish questions.
*
* @package core_backup
* @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
*/
class restore_questionbanks_parser_processor extends grouped_parser_processor {
/**
* Store the restore ID and register paths.
*
* @param string $restoreid ID of the backup being restored.
*/
public function __construct(
/** @var string ID of the backup being restored */
protected string $restoreid,
) {
parent::__construct();
$this->add_path('/activity');
}
#[\Override]
protected function dispatch_chunk($data): void {
// Recieved one chunk, store the context ID as that's what we will match question categories against.
$itemid = $data['tags']['contextid'];
restore_dbops::set_backup_ids_record($this->restoreid, 'questionbank', $itemid);
}
#[\Override]
protected function notify_path_start($path) {
// Nothing to do.
}
#[\Override]
protected function notify_path_end($path) {
// Nothing to do.
}
}
@@ -39,6 +39,7 @@ require_once($CFG->dirroot . '/backup/util/helper/backup_anonymizer_helper.class
require_once($CFG->dirroot . '/backup/util/helper/backup_file_manager.class.php');
require_once($CFG->dirroot . '/backup/util/helper/copy_helper.class.php');
require_once($CFG->dirroot . '/backup/util/helper/restore_prechecks_helper.class.php');
require_once($CFG->dirroot . '/backup/util/helper/restore_questionbanks_parser_processor.php');
require_once($CFG->dirroot . '/backup/util/helper/restore_moodlexml_parser_processor.class.php');
require_once($CFG->dirroot . '/backup/util/helper/restore_inforef_parser_processor.class.php');
require_once($CFG->dirroot . '/backup/util/helper/restore_users_parser_processor.class.php');
+236
View File
@@ -0,0 +1,236 @@
<?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 mod_qbank\backup;
use core\context\module;
/**
* Tests to cover restoring or import a question bank and its questions multiple times to the same course.
*
* When we restore/import/duplicate an activity that publishes questions, all of its questions should
* be restored into the restored activity, even if they already exist in another activity on the same course.
*
* @package mod_qbank
* @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_dbops::load_questionbanks_to_tempids
* @covers \restore_questionbanks_parser_processor
*/
final class restore_test extends \advanced_testcase {
/**
* Given a context, find the default question category, return the question IDs and records.
*
* @param int $contextid
* @return array[int[], \stdClass[]] An array of the question ID, and an numerically-indexed array of question records.
*/
protected function get_questions_in_default_category(int $contextid): array {
global $DB;
$records = $DB->get_records_sql(
"
SELECT q.*
FROM {question} q
JOIN {question_versions} qv ON q.id = qv.questionid
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
WHERE qc.contextid = :contextid
AND qc.name != :top
",
[
'contextid' => $contextid,
'top' => 'top',
],
);
return [array_keys($records), array_values($records)];
}
/**
* Create a qbank containing questions and verify the correct records exist.
*
* @param int $courseid
* @return array
*/
protected function create_qbank_with_questions(int $courseid): array {
// Create a quiz with questions in the first course.
$qbank = $this->getDataGenerator()->get_plugin_generator('mod_qbank')->create_instance(['course' => $courseid]);
$context = \context_module::instance($qbank->cmid);
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = question_get_default_category($context->id);
// Create a short answer question.
$saq = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
// Create a multi-answer question with children.
$maq = $questiongenerator->create_question('multianswer', 'twosubq', ['category' => $cat->id]);
// Verify that we have 1 qbank, with a default category containing 4 questions (2, plus 2 children).
$qbanks = get_fast_modinfo($courseid)->get_instances_of('qbank');
$this->assertCount(1, $qbanks);
$qbank1 = reset($qbanks);
$qbankcontext = module::instance($qbank1->id);
[, $qbankquestions] = $this->get_questions_in_default_category($qbankcontext->id);
$this->assertCount(4, $qbankquestions);
return [$qbank, $saq, $maq];
}
/**
* Importing (duplicating) a bank in the same course should give you a second bank with a copy of all the questions.
*/
public function test_import_qbank_into_same_course(): void {
global $CFG, $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();
$generator = $this->getDataGenerator();
$course1 = $generator->create_course();
$teacher = $USER;
$generator->enrol_user($teacher->id, $course1->id, 'editingteacher');
[$qbank, $originalsaq, $originalmaq] = $this->create_qbank_with_questions($course1->id);
// Backup qbank.
$bc = new \backup_controller(
\backup::TYPE_1ACTIVITY,
$qbank->cmid,
\backup::FORMAT_MOODLE,
\backup::INTERACTIVE_NO,
\backup::MODE_IMPORT,
$teacher->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,
$teacher->id,
\backup::TARGET_CURRENT_ADDING,
);
$rc->execute_precheck();
$rc->execute_plan();
$rc->destroy();
// Verify that we now have 2 qbanks.
$qbanks = get_fast_modinfo($course1->id)->get_instances_of('qbank');
$this->assertCount(2, $qbanks);
// The first qbank should be the same as before.
$qbank1 = reset($qbanks);
$qbank1context = module::instance($qbank1->id);
[$qbank1questionids, $qbank1questions] = $this->get_questions_in_default_category($qbank1context->id);
$this->assertCount(4, $qbank1questions);
$this->assertContains((int) $originalsaq->id, $qbank1questionids);
$this->assertContains((int) $originalmaq->id, $qbank1questionids);
// The second qbank should have its own categories and a copy of each question.
$qbank2 = end($qbanks);
$qbank2context = module::instance($qbank2->id);
[$qbank2questionids, $qbank2questions] = $this->get_questions_in_default_category($qbank2context->id);
$this->assertCount(4, $qbank2questions);
foreach ($qbank2questions as $key => $qbank2question) {
$this->assertNotContains((int) $qbank2question->id, $qbank1questionids);
$this->assertEquals($qbank2question->questiontext, $qbank1questions[$key]->questiontext);
if ($qbank2question->parent != 0) {
// Check that child questions are linked to the parent in the same qbank.
$this->assertNotContains((int) $qbank2question->parent, $qbank1questionids);
$this->assertContains((int) $qbank2question->parent, $qbank2questionids);
}
}
}
/**
* Importing a bank into a different course multiple times should copy all the questions each time.
*/
public function test_import_qbank_into_different_course_twice(): void {
global $CFG, $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();
$generator = $this->getDataGenerator();
$course1 = $generator->create_course();
$course2 = $generator->create_course();
$teacher = $USER;
$generator->enrol_user($teacher->id, $course1->id, 'editingteacher');
$generator->enrol_user($teacher->id, $course2->id, 'editingteacher');
[$qbank, $originalsaq, $originalmaq] = $this->create_qbank_with_questions($course1->id);
for ($i = 0, $j = 2; $i < $j; $i++) {
// Backup qbank.
$bc = new \backup_controller(
\backup::TYPE_1ACTIVITY,
$qbank->cmid,
\backup::FORMAT_MOODLE,
\backup::INTERACTIVE_NO,
\backup::MODE_IMPORT,
$teacher->id,
);
$backupid = $bc->get_backupid();
$bc->execute_plan();
$bc->destroy();
// Restore the backup into another course twice.
$rc = new \restore_controller(
$backupid,
$course2->id,
\backup::INTERACTIVE_NO,
\backup::MODE_IMPORT,
$teacher->id,
\backup::TARGET_CURRENT_ADDING,
);
$rc->execute_precheck();
$rc->execute_plan();
$rc->destroy();
}
$qbankcontext = module::instance($qbank->cmid);
[$seenquestionids] = $this->get_questions_in_default_category($qbankcontext->id);
// Verify that we have 2 qbanks on the destination course, each with its own categories and questions.
$qbanks = get_fast_modinfo($course2->id)->get_instances_of('qbank');
$this->assertCount(2, $qbanks);
foreach ($qbanks as $qbank) {
$qbankcontext = module::instance($qbank->id);
[$qbankquestionids, $qbankquestions] = $this->get_questions_in_default_category($qbankcontext->id);
$this->assertCount(4, $qbankquestions);
// The new question bank doesn't contain the original questions.
$this->assertNotContains((int) $originalsaq->id, $qbankquestionids);
$this->assertNotContains((int) $originalmaq->id, $qbankquestionids);
// The new question bank doesn't contain questions from any other question bank.
$this->assertEmpty(array_intersect($seenquestionids, $qbankquestionids));
$seenquestionids = array_merge($seenquestionids, $qbankquestionids);
// The new question bank does contain its own copy of the questions.
$this->assertNotEmpty(
array_filter($qbankquestions, fn($question) => $question->questiontext == $originalsaq->questiontext)
);
$qbankmaq = array_filter($qbankquestions, fn($question) => $question->questiontext == $originalmaq->questiontext);
$qbankmaq = reset($qbankmaq);
$this->assertNotFalse($qbankmaq);
// It also contains 2 children of the multianswer question.
$this->assertCount(2, array_filter($qbankquestions, fn($question) => $question->parent == $qbankmaq->id));
}
}
}
+375 -24
View File
@@ -46,14 +46,14 @@ final class repeated_restore_test extends advanced_testcase {
use quiz_question_helper_test_trait;
/**
* Restore a quiz twice into the same target course, and verify the quiz uses the restored questions both times.
* Create 2 courses, and a quiz with questions on the first course.
*
* @param bool $sharedquestions If true, create the questions in a qbank module rather than the quiz itself.
* @return array
*/
public function test_restore_quiz_into_other_course_twice(): void {
protected function create_courses_and_quiz(bool $sharedquestions = false): array {
global $USER;
$this->resetAfterTest();
$this->setAdminUser();
// Step 1: Create two courses and a user with editing teacher capabilities.
// Create two courses and a user with editing teacher capabilities.
$generator = $this->getDataGenerator();
$course1 = $generator->create_course();
$course2 = $generator->create_course();
@@ -63,8 +63,12 @@ final class repeated_restore_test extends advanced_testcase {
// Create a quiz with questions in the first course.
$quiz = $this->create_test_quiz($course1);
$qbank = $generator->get_plugin_generator('mod_qbank')->create_instance(['course' => $course1->id]);
$context = \context_module::instance($qbank->cmid);
if ($sharedquestions) {
$qbank = $generator->get_plugin_generator('mod_qbank')->create_instance(['course' => $course1->id]);
$context = \context_module::instance($qbank->cmid);
} else {
$context = \context_module::instance($quiz->cmid);
}
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
// Create a question category.
@@ -97,23 +101,56 @@ final class repeated_restore_test extends advanced_testcase {
$modules1 = get_fast_modinfo($course1->id)->get_instances_of('quiz');
$module1 = reset($modules1);
$questionscourse1 = \mod_quiz\question\bank\qbank_helper::get_question_structure(
$module1->instance, $module1->context);
$module1->instance,
$module1->context,
);
$originalquestionids = [];
foreach ($questionscourse1 as $slot) {
array_push($originalquestionids, intval($slot->questionid));
}
// Step 2: Backup the first course.
$bc = new backup_controller(backup::TYPE_1COURSE, $course1->id, backup::FORMAT_MOODLE,
backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id);
return [
$course1,
$course2,
$quiz,
$teacher,
$originalquestionids,
];
}
/**
* Restore a quiz using private questions twice into the same target course,
* and verify the quiz uses a newly-restored copy of the questions each time.
*/
public function test_restore_quiz_with_own_questions_into_other_course_twice(): void {
$this->resetAfterTest();
$this->setAdminUser();
[, $course2, $quiz, $teacher, $originalquestionids] = $this->create_courses_and_quiz();
// Backup the quiz course.
$bc = new backup_controller(
backup::TYPE_1ACTIVITY,
$quiz->cmid,
backup::FORMAT_MOODLE,
backup::INTERACTIVE_NO,
backup::MODE_IMPORT,
$teacher->id,
);
$backupid = $bc->get_backupid();
$bc->execute_plan();
$bc->destroy();
// Step 3: Import the backup into the second course.
$rc = new restore_controller($backupid, $course2->id, backup::INTERACTIVE_NO, backup::MODE_IMPORT,
$teacher->id, backup::TARGET_CURRENT_ADDING);
// Import the backup into the second course.
$rc = new restore_controller(
$backupid,
$course2->id,
backup::INTERACTIVE_NO,
backup::MODE_IMPORT,
$teacher->id,
backup::TARGET_CURRENT_ADDING,
);
$rc->execute_precheck();
$rc->execute_plan();
$rc->destroy();
@@ -123,34 +160,348 @@ final class repeated_restore_test extends advanced_testcase {
$modules2 = get_fast_modinfo($course2->id)->get_instances_of('quiz');
$module2 = reset($modules2);
$questionscourse2firstimport = \mod_quiz\question\bank\qbank_helper::get_question_structure(
$module2->instance, $module2->context);
$module2->instance,
$module2->context,
);
foreach ($questionscourse2firstimport as $slot) {
$this->assertNotContains(intval($slot->questionid), $originalquestionids,
"Question ID $slot->questionid should not be in the original course's question IDs.");
$this->assertNotContains(
intval($slot->questionid),
$originalquestionids,
"Question ID $slot->questionid should not be in the original course's question IDs.",
);
}
// Repeat the backup and import process to simulate a second import.
$bc = new backup_controller(backup::TYPE_1COURSE, $course1->id, backup::FORMAT_MOODLE,
backup::INTERACTIVE_NO, backup::MODE_IMPORT, $teacher->id);
$bc = new backup_controller(
backup::TYPE_1ACTIVITY,
$quiz->cmid,
backup::FORMAT_MOODLE,
backup::INTERACTIVE_NO,
backup::MODE_IMPORT,
$teacher->id,
);
$backupid = $bc->get_backupid();
$bc->execute_plan();
$bc->destroy();
$rc = new restore_controller($backupid, $course2->id, backup::INTERACTIVE_NO, backup::MODE_IMPORT,
$teacher->id, backup::TARGET_CURRENT_ADDING);
$rc = new restore_controller(
$backupid,
$course2->id,
backup::INTERACTIVE_NO,
backup::MODE_IMPORT,
$teacher->id,
backup::TARGET_CURRENT_ADDING,
);
$rc->execute_precheck();
$rc->execute_plan();
$rc->destroy();
// Verify that the second restore has used the same new questions that were created by the first restore.
// Verify that the quiz in the second restore uses a third copy of the questions.
$modules3 = get_fast_modinfo($course2->id)->get_instances_of('quiz');
$module3 = end($modules3);
$questionscourse2secondimport = \mod_quiz\question\bank\qbank_helper::get_question_structure(
$module3->instance, $module3->context);
$module3->instance,
$module3->context,
);
foreach ($questionscourse2secondimport as $slot) {
$this->assertNotEquals($questionscourse2firstimport[$slot->slot]->questionid, $slot->questionid);
$this->assertNotContains(
intval($slot->questionid),
$originalquestionids,
"Question ID $slot->questionid should not be in the original course's question IDs.",
);
}
}
/**
* Restore a quiz using shared questions twice into the same target course,
* and verify the quiz uses the original questions each time.
*/
public function test_restore_quiz_with_shared_questions_into_other_course_twice(): void {
$this->resetAfterTest();
$this->setAdminUser();
[, $course2, $quiz, $teacher, $originalquestionids] = $this->create_courses_and_quiz(true);
// Backup the quiz.
$bc = new backup_controller(
backup::TYPE_1ACTIVITY,
$quiz->cmid,
backup::FORMAT_MOODLE,
backup::INTERACTIVE_NO,
backup::MODE_IMPORT,
$teacher->id,
);
$backupid = $bc->get_backupid();
$bc->execute_plan();
$bc->destroy();
// Import the backup into the second course.
$rc = new restore_controller(
$backupid,
$course2->id,
backup::INTERACTIVE_NO,
backup::MODE_IMPORT,
$teacher->id,
backup::TARGET_CURRENT_ADDING,
);
$rc->execute_precheck();
$rc->execute_plan();
$rc->destroy();
// Verify the question ids from the quiz in the original course are the same as
// the question ids in the duplicated quiz in the second course.
$modules2 = get_fast_modinfo($course2->id)->get_instances_of('quiz');
$module2 = reset($modules2);
$questionscourse2firstimport = \mod_quiz\question\bank\qbank_helper::get_question_structure(
$module2->instance,
$module2->context,
);
foreach ($questionscourse2firstimport as $slot) {
$this->assertContains(
intval($slot->questionid),
$originalquestionids,
"Question ID $slot->questionid should be in the original course's question IDs.",
);
}
// Repeat the backup and import process to simulate a second import.
$bc = new backup_controller(
backup::TYPE_1ACTIVITY,
$quiz->cmid,
backup::FORMAT_MOODLE,
backup::INTERACTIVE_NO,
backup::MODE_IMPORT,
$teacher->id,
);
$backupid = $bc->get_backupid();
$bc->execute_plan();
$bc->destroy();
$rc = new restore_controller(
$backupid,
$course2->id,
backup::INTERACTIVE_NO,
backup::MODE_IMPORT,
$teacher->id,
backup::TARGET_CURRENT_ADDING,
);
$rc->execute_precheck();
$rc->execute_plan();
$rc->destroy();
// Verify that the quiz in the second restore uses the same questions from the original quiz and the first restore.
$modules3 = get_fast_modinfo($course2->id)->get_instances_of('quiz');
$module3 = end($modules3);
$questionscourse2secondimport = \mod_quiz\question\bank\qbank_helper::get_question_structure(
$module3->instance,
$module3->context,
);
foreach ($questionscourse2secondimport as $slot) {
$this->assertEquals($questionscourse2firstimport[$slot->slot]->questionid, $slot->questionid);
$this->assertContains(
intval($slot->questionid),
$originalquestionids,
"Question ID $slot->questionid should be in the original course's question IDs.",
);
}
}
/**
* Restore a quiz using shared questions twice into the same target course with the qbank,
* and verify the quiz uses a newly-restored copy of the questions each time.
*/
public function test_restore_quiz_with_qbank_into_other_course_twice(): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
[$course1, $course2, , $teacher, $originalquestionids] = $this->create_courses_and_quiz(true);
// Backup the first course.
$bc = new backup_controller(
backup::TYPE_1COURSE,
$course1->id,
backup::FORMAT_MOODLE,
backup::INTERACTIVE_NO,
backup::MODE_IMPORT,
$teacher->id,
);
$backupid = $bc->get_backupid();
$bc->execute_plan();
$bc->destroy();
// Import the backup into the second course.
$rc = new restore_controller(
$backupid,
$course2->id,
backup::INTERACTIVE_NO,
backup::MODE_IMPORT,
$teacher->id,
backup::TARGET_CURRENT_ADDING,
);
$rc->execute_precheck();
$rc->execute_plan();
$rc->destroy();
// Verify the question ids from the quiz in the original course are different
// from the question ids in the duplicated quiz in the second course.
$modules2 = get_fast_modinfo($course2->id)->get_instances_of('quiz');
$module2 = reset($modules2);
$questionscourse2firstimport = \mod_quiz\question\bank\qbank_helper::get_question_structure(
$module2->instance,
$module2->context,
);
foreach ($questionscourse2firstimport as $slot) {
$this->assertNotContains(
intval($slot->questionid),
$originalquestionids,
"Question ID $slot->questionid should not be in the original course's question IDs.",
);
}
// Repeat the backup and import process to simulate a second import.
$bc = new backup_controller(
backup::TYPE_1COURSE,
$course1->id,
backup::FORMAT_MOODLE,
backup::INTERACTIVE_NO,
backup::MODE_IMPORT,
$teacher->id,
);
$backupid = $bc->get_backupid();
$bc->execute_plan();
$bc->destroy();
$rc = new restore_controller(
$backupid,
$course2->id,
backup::INTERACTIVE_NO,
backup::MODE_IMPORT,
$teacher->id,
backup::TARGET_CURRENT_ADDING,
);
$rc->execute_precheck();
$rc->execute_plan();
$rc->destroy();
// Verify that the quiz in the second restore uses a third copy of the questions.
$modules3 = get_fast_modinfo($course2->id)->get_instances_of('quiz');
$module3 = end($modules3);
$questionscourse2secondimport = \mod_quiz\question\bank\qbank_helper::get_question_structure(
$module3->instance,
$module3->context,
);
foreach ($questionscourse2secondimport as $slot) {
$this->assertNotEquals($questionscourse2firstimport[$slot->slot]->questionid, $slot->questionid);
$this->assertNotContains(
intval($slot->questionid),
$originalquestionids,
"Question ID $slot->questionid should not be in the original course's question IDs.",
);
}
}
/**
* Import the quiz and qbank from course 1 to course 2, then import just the quiz a second time.
*
* As the user can use questions from the original qbank, the second quiz will use the original questions.
*/
public function test_restore_quiz_with_shared_questions_then_just_quiz(): void {
$this->resetAfterTest();
$this->setAdminUser();
[$course1, $course2, $quiz, $teacher, $originalquestionids] = $this->create_courses_and_quiz(true);
// Backup the first course.
$bc = new backup_controller(
backup::TYPE_1COURSE,
$course1->id,
backup::FORMAT_MOODLE,
backup::INTERACTIVE_NO,
backup::MODE_IMPORT,
$teacher->id,
);
$backupid = $bc->get_backupid();
$bc->execute_plan();
$bc->destroy();
// Import the backup into the second course.
$rc = new restore_controller(
$backupid,
$course2->id,
backup::INTERACTIVE_NO,
backup::MODE_IMPORT,
$teacher->id,
backup::TARGET_CURRENT_ADDING,
);
$rc->execute_precheck();
$rc->execute_plan();
$rc->destroy();
// Verify the question ids from the quiz in the original course are different
// from the question ids in the duplicated quiz in the second course.
$modules2 = get_fast_modinfo($course2->id)->get_instances_of('quiz');
$module2 = reset($modules2);
$questionscourse2firstimport = \mod_quiz\question\bank\qbank_helper::get_question_structure(
$module2->instance,
$module2->context,
);
foreach ($questionscourse2firstimport as $slot) {
$this->assertNotContains(
intval($slot->questionid),
$originalquestionids,
"Question ID $slot->questionid should not be in the original course's question IDs.",
);
}
// Repeat the backup and import process with just the quiz.
$bc = new backup_controller(
backup::TYPE_1ACTIVITY,
$quiz->cmid,
backup::FORMAT_MOODLE,
backup::INTERACTIVE_NO,
backup::MODE_IMPORT,
$teacher->id,
);
$backupid = $bc->get_backupid();
$bc->execute_plan();
$bc->destroy();
$rc = new restore_controller(
$backupid,
$course2->id,
backup::INTERACTIVE_NO,
backup::MODE_IMPORT,
$teacher->id,
backup::TARGET_CURRENT_ADDING,
);
$rc->execute_precheck();
$rc->execute_plan();
$rc->destroy();
// Verify that the quiz in the second restore uses the questions from the original course.
$modules3 = get_fast_modinfo($course2->id)->get_instances_of('quiz');
$module3 = end($modules3);
$questionscourse2secondimport = \mod_quiz\question\bank\qbank_helper::get_question_structure(
$module3->instance,
$module3->context,
);
foreach ($questionscourse2secondimport as $slot) {
$this->assertNotEquals($questionscourse2firstimport[$slot->slot]->questionid, $slot->questionid);
$this->assertContains(
intval($slot->questionid),
$originalquestionids,
"Question ID $slot->questionid should be in the original course's question IDs.",
);
}
}
+2 -2
View File
@@ -890,7 +890,7 @@ final class backup_test extends \advanced_testcase {
quiz_add_quiz_question($questionv2->id, $testdata->quiz);
$structure1->update_slot_version($structure1->get_slot_id_for_slot(1), 2);
$backupid = $this->backup_course($testdata->course);
$backupid = $this->backup_course_module($testdata->quiz->cmid);
question_delete_question($questionv4->id); // Actually deleted.
question_delete_question($questionv3->id); // Actually deleted.
@@ -1006,7 +1006,7 @@ final class backup_test extends \advanced_testcase {
quiz_add_quiz_question($questionv2->id, $testdata->quiz);
$structure1->update_slot_version($structure1->get_slot_id_for_slot(1), 2);
$backupid = $this->backup_course($testdata->course);
$backupid = $this->backup_course_module($testdata->quiz->cmid);
question_delete_question($questionv4->id); // Actually deleted.
question_delete_question($questionv3->id); // Actually deleted.