MDL-84037 quiz: Fix restore of shared questions

When a quiz that used shared questions was being restored without the
qbank the questions came from, if the qbank still existed on the target
site, the restored questions were being left in a category in an invalid
course context.

This changes the process so that if the original qbank does exist and
the user can access to it, we will find any references to the restored
copies of questions from that qbank, switch them to refer back to the
original qbank, then delete the category they were restored to from the
course context.

If the user does not have access to the qbank, a new one will be created
in the target course and the questions moved there, just as if the
original qbank did not exist.
This commit is contained in:
Mark Johnson 2024-12-19 16:07:14 +00:00
parent a97ddeb2a2
commit 89b061a702
No known key found for this signature in database
GPG Key ID: EB30E1468CFAE242
2 changed files with 147 additions and 5 deletions

View File

@ -5482,8 +5482,25 @@ class restore_move_module_questions_categories extends restore_execution_step {
foreach ($contexts as $contextid => $contextlevel) {
if (!$newcontext = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'context', $contextid)) {
// The bank for the question categories required by this module was not included in the backup,
// but if that context still exists on the site then don't move them.
if (context::instance_by_id($contextid, IGNORE_MISSING)) {
// but if that context still exists on the site and the user has access then point question references
// to the originals.
$originalcontext = context::instance_by_id($contextid, IGNORE_MISSING);
if ($originalcontext && has_capability('mod/qbank:view', $originalcontext)) {
$originalquestions = get_questions_category(question_get_top_category($contextid), false);
foreach ($originalquestions as $originalquestion) {
$backupids = restore_dbops::get_backup_ids_record(
$this->get_restoreid(),
'question',
$originalquestion->id,
);
$DB->set_field_select(
'question_references',
'questionbankentryid',
$DB->get_field('question_versions', 'questionbankentryid', ['questionid' => $backupids->itemid]),
'questionbankentryid = (SELECT questionbankentryid FROM {question_versions} WHERE questionid = ?)',
[$backupids->newitemid],
);
}
continue;
}
// We have no target question bank so create a default bank for categories without a module to attach to.
@ -5588,6 +5605,20 @@ class restore_move_module_questions_categories extends restore_execution_step {
);
}
}
// Remove any remaining course-level question categories from the restored course.
$coursecatsql = "
SELECT qc.id AS categoryid
FROM {question_categories} qc
JOIN {context} c ON c.id = qc.contextid
WHERE c.contextlevel = :courselevel AND c.instanceid = :courseid
";
$DB->delete_records_subquery(
'question_categories',
'id',
'categoryid',
$coursecatsql,
['courselevel' => context_course::LEVEL, 'courseid' => $this->task->get_courseid()]
);
}
}

View File

@ -17,6 +17,7 @@
namespace core_question;
use context_course;
use mod_quiz\quiz_settings;
use moodle_url;
use question_bank;
@ -40,6 +41,7 @@ require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \restore_qtype_plugin
* @covers \restore_create_categories_and_questions
* @covers \restore_move_module_questions_categories
*/
final class backup_test extends \advanced_testcase {
@ -545,11 +547,9 @@ final class backup_test extends \advanced_testcase {
/**
* If the backup contains ONLY a quiz but that quiz uses questions from a qbank module and itself,
* and the original course does not exist on the target system,
* then the non-quiz context categories and questions should restore to a default qbank module on the new course
* if the old qbank no longer exists.
*
* @return void
* @covers \restore_controller::execute_plan()
*/
public function test_quiz_activity_restore_to_new_course(): void {
global $DB;
@ -605,6 +605,117 @@ final class backup_test extends \advanced_testcase {
$this->assertEquals('bank question', $bankq->name);
}
/**
* If the backup contains ONLY a quiz but that quiz uses questions from a qbank module and itself,
* and the original course does exist on the target system but you dont have permission to view the original qbank,
* then the non-quiz context categories and questions should restore to a default qbank module on the new course
* if the old qbank no longer exists.
*/
public function test_quiz_activity_restore_to_new_course_no_permission(): void {
global $DB;
$this->resetAfterTest();
self::setAdminUser();
// Create a course to make a backup.
$data = $this->add_course_quiz_and_qbank();
$oldquiz = $data->quiz;
// Backup ONLY the quiz module.
$backupid = $this->backup_course_module($oldquiz->cmid);
// Create a new course to restore to.
$newcourse = self::getDataGenerator()->create_course();
$restoreuser = self::getDataGenerator()->create_user();
self::getDataGenerator()->enrol_user($restoreuser->id, $newcourse->id, 'manager');
$this->setUser($restoreuser);
$this->restore_to_course($backupid, $newcourse->id);
$modinfo = get_fast_modinfo($newcourse);
// Assert we have our quiz including the category and question.
$newquizzes = $modinfo->get_instances_of('quiz');
$this->assertCount(1, $newquizzes);
$newquiz = reset($newquizzes);
$newquizcontext = \context_module::instance($newquiz->id);
$quizcats = $DB->get_records_select('question_categories',
'parent <> 0 AND contextid = :contextid',
['contextid' => $newquizcontext->id]
);
$this->assertCount(1, $quizcats);
$quizcat = reset($quizcats);
$quizcatqs = get_questions_category($quizcat, false);
$this->assertCount(1, $quizcatqs);
$quizq = reset($quizcatqs);
$this->assertEquals('quiz question', $quizq->name);
// The backup did not contain the qbank that held the categories, but it is dependant.
// So make sure the categories and questions got restored to a qbank module on the course.
$defaultbanks = $modinfo->get_instances_of('qbank');
$this->assertCount(1, $defaultbanks);
$defaultbank = reset($defaultbanks);
$defaultbankcontext = \context_module::instance($defaultbank->id);
$bankcats = $DB->get_records_select('question_categories',
'parent <> 0 AND contextid = :contextid',
['contextid' => $defaultbankcontext->id]
);
$bankcat = reset($bankcats);
$bankqs = get_questions_category($bankcat, false);
$this->assertCount(1, $bankqs);
$bankq = reset($bankqs);
$this->assertEquals('bank question', $bankq->name);
}
/**
* If the backup contains ONLY a quiz but that quiz uses questions from a qbank module and itself,
* and that qbank still exists on the system, and the restoring user can access that qbank, then
* the quiz should be restored with a copy of the quiz question, and a reference to the original qbank question.
*/
public function test_quiz_activity_restore_to_new_course_by_reference(): void {
global $DB;
$this->resetAfterTest();
self::setAdminUser();
// Create a course to make a backup.
$data = $this->add_course_quiz_and_qbank();
$oldquiz = $data->quiz;
// Backup ONLY the quiz module.
$backupid = $this->backup_course_module($oldquiz->cmid);
// Create a new course to restore to.
$newcourse = self::getDataGenerator()->create_course();
$this->restore_to_course($backupid, $newcourse->id);
$modinfo = get_fast_modinfo($newcourse);
// Assert we have our new quiz with the expected questions.
$newquizzes = $modinfo->get_instances_of('quiz');
$this->assertCount(1, $newquizzes);
/** @var \cm_info $newquiz */
$newquiz = reset($newquizzes);
$quiz = $DB->get_record('quiz', ['id' => $newquiz->instance], '*', MUST_EXIST);
[$course, $cm] = get_course_and_cm_from_instance($quiz, 'quiz');
$newquizsettings = new quiz_settings($quiz, $cm, $course);
$newq1 = $newquizsettings->get_structure()->get_question_in_slot(1);
$newq2 = $newquizsettings->get_structure()->get_question_in_slot(2);
$newquizcontext = \context_module::instance($newquiz->id);
$qbankcontext = \context_module::instance($data->qbank->cmid);
// Check we've got a copy of the quiz question in the new context.
$this->assertEquals($data->quizquestion->name, $newq2->name);
$this->assertEquals($newquizcontext->id, $newq2->contextid);
// Check we've got a reference to the qbank question in the original context.
$this->assertEquals($data->qbankquestion->name, $newq1->name);
$this->assertEquals($qbankcontext->id, $newq1->contextid);
// Check we have the expected restored categories.
$this->assertEquals(2, $DB->count_records('question_categories', ['stamp' => $data->quizcategory->stamp]));
$this->assertEquals(1, $DB->count_records('question_categories', ['stamp' => $data->qbankcategory->stamp]));
}
/**
* If the backup contains BOTH a quiz and a qbank module and the quiz uses questions from the qbank module and itself,
* then we need to restore the categories and questions to the qbank and quiz modules included in the backup on the new course.