Merge branch 'MDL-84728_main' of https://github.com/marxjohnson/moodle
This commit is contained in:
@@ -6394,6 +6394,7 @@ trait restore_question_set_reference_data_trait {
|
||||
public function process_question_set_reference($data) {
|
||||
global $DB;
|
||||
$data = (object) $data;
|
||||
$owncontext = $data->usingcontextid == $data->questionscontextid;
|
||||
$data->usingcontextid = $this->get_mappingid('context', $data->usingcontextid);
|
||||
$data->itemid = $this->get_new_parentid('quiz_question_instance');
|
||||
$filtercondition = json_decode($data->filtercondition, true);
|
||||
@@ -6406,8 +6407,22 @@ trait restore_question_set_reference_data_trait {
|
||||
|
||||
// Map category id used for category filter condition and corresponding context id.
|
||||
$oldcategoryid = $filtercondition['filter']['category']['values'][0];
|
||||
$newcategoryid = $this->get_mappingid('question_category', $oldcategoryid);
|
||||
$filtercondition['filter']['category']['values'][0] = $newcategoryid;
|
||||
// Decide if we're going to refer back to the original category, or to the new category.
|
||||
// Are we restoring to a different site?
|
||||
// Has the original context or category been deleted?
|
||||
// Did the old category belong to the same context as the original set reference?
|
||||
// Are we allowed to use its questions?
|
||||
$questionscontext = context::instance_by_id($data->questionscontextid, IGNORE_MISSING);
|
||||
if (
|
||||
!$this->get_task()->is_samesite()
|
||||
|| !$questionscontext
|
||||
|| !$DB->record_exists('question_categories', ['id' => $oldcategoryid])
|
||||
|| $owncontext
|
||||
|| !has_capability('moodle/question:useall', $questionscontext)
|
||||
) {
|
||||
$newcategoryid = $this->get_mappingid('question_category', $oldcategoryid);
|
||||
$filtercondition['filter']['category']['values'][0] = $newcategoryid;
|
||||
}
|
||||
|
||||
if ($context = $this->get_mappingid('context', $data->questionscontextid)) {
|
||||
$data->questionscontextid = $context;
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
namespace mod_quiz;
|
||||
|
||||
use core\exception\coding_exception;
|
||||
use core_question\local\bank\random_question_loader;
|
||||
use core_question\question_reference_manager;
|
||||
use mod_quiz\question\display_options;
|
||||
|
||||
@@ -34,6 +36,9 @@ require_once($CFG->dirroot . '/mod/quiz/locallib.php');
|
||||
* @copyright 2021 Catalyst IT Australia Pty Ltd
|
||||
* @author Safat Shahin <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @covers \mod_quiz\question\bank\qbank_helper
|
||||
* @covers \restore_quiz_activity_structure_step
|
||||
* @covers \restore_question_set_reference_data_trait
|
||||
*/
|
||||
final class quiz_question_restore_test extends \advanced_testcase {
|
||||
use \quiz_question_helper_test_trait;
|
||||
@@ -56,21 +61,115 @@ final class quiz_question_restore_test extends \advanced_testcase {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a quiz with 2 questions and 1 random question, using question categories in the provided context.
|
||||
*
|
||||
* @covers \mod_quiz\question\bank\qbank_helper::get_question_structure
|
||||
* @param int $questioncontextid Context to create question categories in.
|
||||
* @return array The quiz, slots, random questions in each slot that has them, and the quiz context.
|
||||
*/
|
||||
public function create_quiz_with_questions(int $questioncontextid): array {
|
||||
$quiz = $this->create_test_quiz($this->course);
|
||||
$quizcontext = \context_module::instance($quiz->cmid);
|
||||
|
||||
// Test for questions from a different context.
|
||||
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
$this->add_two_regular_questions($questiongenerator, $quiz, ['contextid' => $questioncontextid]);
|
||||
$this->add_one_random_question($questiongenerator, $quiz, ['contextid' => $questioncontextid]);
|
||||
$slots = \mod_quiz\question\bank\qbank_helper::get_question_structure(
|
||||
$quiz->id, $quizcontext);
|
||||
$randomquestions = [];
|
||||
$loader = new random_question_loader(new \qubaid_list([]));
|
||||
foreach ($slots as $number => $slot) {
|
||||
if (!empty($slot->filtercondition)) {
|
||||
$randomquestions[$number] = $loader->get_filtered_questions($slot->filtercondition['filter']);
|
||||
}
|
||||
}
|
||||
return [
|
||||
$quiz,
|
||||
$slots,
|
||||
$randomquestions,
|
||||
$quizcontext,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the layout of the quiz on the provided course matches (or doesn't match) the expected layout.
|
||||
*
|
||||
* For $expectedslots, the value should be provided as an array keyed by slot number. Each slot can be provided as a single
|
||||
* number, which will considered a question ID, or an array with 'filter' and 'randomquestionids' which will be considered
|
||||
* a random question.
|
||||
*
|
||||
* For example,
|
||||
* [
|
||||
* [1] => 123,
|
||||
* [2] => [
|
||||
* 'filter' => ['name' => 'category', 'values' => ['345']],
|
||||
* 'randomquestionids' => ['101', '102'],
|
||||
* ],
|
||||
* [3] => 124,
|
||||
* ]
|
||||
* Will assert that the quiz contains (or doesn't contain if $expectequal is false):
|
||||
* - A question in slot 1 with ID 123,
|
||||
* - A random question in slot 2 with a filter for category ID 345, which matches questions with IDs 101 and 102,
|
||||
* - A question in slot 3 with ID 124.
|
||||
*
|
||||
* @param int $courseid The ID of the course to pick the quiz from.
|
||||
* @param array $expectedslots The layout of slots to compare to, see above for details of the structure.
|
||||
* @param bool $expectequal If true, assert that each slot is equal to the expected value. Otherwise assert it's not equal.
|
||||
*/
|
||||
public function verify_restored_quiz_layout(
|
||||
int $courseid,
|
||||
array $expectedslots,
|
||||
bool $expectequal
|
||||
): void {
|
||||
$loader = new random_question_loader(new \qubaid_list([]));
|
||||
$modules = get_fast_modinfo($courseid)->get_instances_of('quiz');
|
||||
$module = reset($modules);
|
||||
$newslots = \mod_quiz\question\bank\qbank_helper::get_question_structure(
|
||||
$module->instance, $module->context);
|
||||
$this->assertCount(count($expectedslots), $newslots);
|
||||
foreach ($newslots as $number => $slot) {
|
||||
$randomquestionids = null;
|
||||
if (is_array($expectedslots[$number])) {
|
||||
// A random question, compare filters and filtered question ids.
|
||||
$actualcontents = $slot->filtercondition['filter'];
|
||||
$expectedcontents = $expectedslots[$number]['filter'];
|
||||
$randomquestions = $loader->get_filtered_questions($slot->filtercondition['filter']);
|
||||
$randomquestionids = array_keys($randomquestions);
|
||||
sort($randomquestionids);
|
||||
sort($expectedslots[$number]['randomquestionids']);
|
||||
} else if (is_numeric($expectedslots[$number])) {
|
||||
// A normal question, compare IDs.
|
||||
$actualcontents = $slot->questionid;
|
||||
$expectedcontents = $expectedslots[$number];
|
||||
} else {
|
||||
throw new coding_exception('Slots must either be a number or an array.');
|
||||
}
|
||||
if ($expectequal) {
|
||||
$this->assertEquals($expectedcontents, $actualcontents);
|
||||
if (!is_null($randomquestionids)) {
|
||||
$this->assertEquals($expectedslots[$number]['randomquestionids'], $randomquestionids);
|
||||
}
|
||||
} else {
|
||||
$this->assertNotEquals($expectedcontents, $actualcontents);
|
||||
if (!is_null($randomquestionids)) {
|
||||
$this->assertNotEquals($expectedslots[$number]['randomquestionids'], $randomquestionids);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore a quiz that used questions from a shared question bank on the same course, after the course is deleted.
|
||||
*
|
||||
* The restored quiz should use new copies of the questions.
|
||||
*/
|
||||
public function test_quiz_restore_in_a_different_course_using_question_bank(): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Create the test quiz.
|
||||
$quiz = $this->create_test_quiz($this->course);
|
||||
$oldquizcontext = \context_module::instance($quiz->cmid);
|
||||
$qbank = self::getDataGenerator()->create_module('qbank', ['course' => $this->course]);
|
||||
$qbankcontext = \context_module::instance($qbank->cmid);
|
||||
// Test for questions from a different context.
|
||||
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
$this->add_two_regular_questions($questiongenerator, $quiz, ['contextid' => $qbankcontext->id]);
|
||||
$this->add_one_random_question($questiongenerator, $quiz, ['contextid' => $qbankcontext->id]);
|
||||
[$quiz, $originalslots, $randomquestions, $quizcontext] = $this->create_quiz_with_questions($qbankcontext->id);
|
||||
|
||||
// Make the backup.
|
||||
$backupid = $this->backup_quiz($quiz, $this->user);
|
||||
@@ -79,19 +178,134 @@ final class quiz_question_restore_test extends \advanced_testcase {
|
||||
delete_course($this->course, false);
|
||||
|
||||
// Check if the questions and associated data are deleted properly.
|
||||
$this->assertEquals(0, count(\mod_quiz\question\bank\qbank_helper::get_question_structure(
|
||||
$quiz->id, $oldquizcontext)));
|
||||
$this->assertEquals(0, count(\mod_quiz\question\bank\qbank_helper::get_question_structure($quiz->id, $quizcontext)));
|
||||
|
||||
// Restore the course.
|
||||
$newcourse = $this->getDataGenerator()->create_course();
|
||||
$this->restore_quiz($backupid, $newcourse, $this->user);
|
||||
|
||||
$this->verify_restored_quiz_layout(
|
||||
courseid: $newcourse->id,
|
||||
expectedslots: [
|
||||
1 => $originalslots[1]->questionid,
|
||||
2 => $originalslots[2]->questionid,
|
||||
3 => [
|
||||
'filter' => $originalslots[3]->filtercondition['filter'],
|
||||
'randomquestionids' => array_keys($randomquestions[3]),
|
||||
],
|
||||
],
|
||||
expectequal: false,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore a quiz that used questions from a shared question bank on the same course.
|
||||
*
|
||||
* The restored quiz should reference the questions in the original question bank.
|
||||
*/
|
||||
public function test_quiz_restore_in_a_different_course_reference_original_question_bank(): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Create the test quiz.
|
||||
$qbank = self::getDataGenerator()->create_module('qbank', ['course' => $this->course]);
|
||||
$qbankcontext = \context_module::instance($qbank->cmid);
|
||||
[$quiz, $originalslots, $randomquestions] = $this->create_quiz_with_questions($qbankcontext->id);
|
||||
|
||||
// Make the backup.
|
||||
$backupid = $this->backup_quiz($quiz, $this->user);
|
||||
|
||||
// Restore the course.
|
||||
$newcourse = $this->getDataGenerator()->create_course();
|
||||
$this->restore_quiz($backupid, $newcourse, $this->user);
|
||||
|
||||
$this->verify_restored_quiz_layout(
|
||||
courseid: $newcourse->id,
|
||||
expectedslots: [
|
||||
1 => $originalslots[1]->questionid,
|
||||
2 => $originalslots[2]->questionid,
|
||||
3 => [
|
||||
'filter' => $originalslots[3]->filtercondition['filter'],
|
||||
'randomquestionids' => array_keys($randomquestions[3]),
|
||||
],
|
||||
],
|
||||
expectequal: true,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore a quiz that used questions from a shared question bank on the same course, as a user who cannot access the course.
|
||||
*
|
||||
* The restored quiz should use new copies of the questions.
|
||||
*/
|
||||
public function test_quiz_restore_in_a_different_course_cant_use_original_question_bank(): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Create the test quiz.
|
||||
$qbank = self::getDataGenerator()->create_module('qbank', ['course' => $this->course]);
|
||||
$qbankcontext = \context_module::instance($qbank->cmid);
|
||||
[$quiz, $originalslots, $randomquestions] = $this->create_quiz_with_questions($qbankcontext->id);
|
||||
|
||||
// Make the backup.
|
||||
$backupid = $this->backup_quiz($quiz, $this->user);
|
||||
|
||||
// Restore the course as a new user without access to the original course.
|
||||
$newcourse = $this->getDataGenerator()->create_course();
|
||||
$newuser = self::getDataGenerator()->create_user();
|
||||
self::getDataGenerator()->enrol_user($newuser->id, $newcourse->id, 'manager');
|
||||
$this->setUser($newuser);
|
||||
$this->restore_quiz($backupid, $newcourse, $newuser);
|
||||
|
||||
$this->verify_restored_quiz_layout(
|
||||
courseid: $newcourse->id,
|
||||
expectedslots: [
|
||||
1 => $originalslots[1]->questionid,
|
||||
2 => $originalslots[2]->questionid,
|
||||
3 => [
|
||||
'filter' => $originalslots[3]->filtercondition['filter'],
|
||||
'randomquestionids' => array_keys($randomquestions[3]),
|
||||
],
|
||||
],
|
||||
expectequal: false,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore a quiz that used questions from a shared question bank on another course, after that other course is deleted.
|
||||
*
|
||||
* The restored quiz should use new copies of the questions.
|
||||
*/
|
||||
public function test_quiz_restore_in_a_different_course_question_course_has_been_deleted(): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Create the test quiz.
|
||||
$qbankcourse = self::getDataGenerator()->create_course();
|
||||
$qbank = self::getDataGenerator()->create_module('qbank', ['course' => $qbankcourse]);
|
||||
$qbankcontext = \context_module::instance($qbank->cmid);
|
||||
[$quiz, $originalslots, $randomquestions] = $this->create_quiz_with_questions($qbankcontext->id);
|
||||
|
||||
// Make the backup.
|
||||
$backupid = $this->backup_quiz($quiz, $this->user);
|
||||
|
||||
// Delete the qbank course.
|
||||
delete_course($qbankcourse, false);
|
||||
|
||||
// Restore the course as a new copy.
|
||||
$newcourse = $this->getDataGenerator()->create_course();
|
||||
$this->restore_quiz($backupid, $newcourse, $this->user);
|
||||
|
||||
// Verify.
|
||||
$modules = get_fast_modinfo($newcourse->id)->get_instances_of('quiz');
|
||||
$module = reset($modules);
|
||||
$questions = \mod_quiz\question\bank\qbank_helper::get_question_structure(
|
||||
$module->instance, $module->context);
|
||||
$this->assertCount(3, $questions);
|
||||
$this->verify_restored_quiz_layout(
|
||||
courseid: $newcourse->id,
|
||||
expectedslots: [
|
||||
1 => $originalslots[1]->questionid,
|
||||
2 => $originalslots[2]->questionid,
|
||||
3 => [
|
||||
'filter' => $originalslots[3]->filtercondition['filter'],
|
||||
'randomquestionids' => array_keys($randomquestions[3]),
|
||||
],
|
||||
],
|
||||
expectequal: false,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -151,26 +365,38 @@ final class quiz_question_restore_test extends \advanced_testcase {
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if a duplicate does not duplicate questions from a shared question bank.
|
||||
* Duplicate a quiz that uses questions from a shared bank on the same course.
|
||||
*
|
||||
* @covers ::duplicate_module
|
||||
* The new quiz should reference the same questions.
|
||||
*/
|
||||
public function test_quiz_duplicate_does_not_duplicate_questions_from_shared_banks(): void {
|
||||
$this->resetAfterTest();
|
||||
$quiz = $this->create_test_quiz($this->course);
|
||||
$qbank = self::getDataGenerator()->create_module('qbank', ['course' => $this->course->id]);
|
||||
// Test for questions from a qbank context.
|
||||
$qbankcourse = self::getDataGenerator()->create_course();
|
||||
$qbank = self::getDataGenerator()->create_module('qbank', ['course' => $qbankcourse]);
|
||||
$context = \context_module::instance($qbank->cmid);
|
||||
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
$this->add_two_regular_questions($questiongenerator, $quiz, ['contextid' => $context->id]);
|
||||
$this->add_one_random_question($questiongenerator, $quiz, ['contextid' => $context->id]);
|
||||
[$quiz, $originalslots, $randomquestions] = $this->create_quiz_with_questions($context->id);
|
||||
// Count the questions in qbank context.
|
||||
$this->assertEquals(7, $this->question_count($context->id));
|
||||
$this->assertCount(3, $originalslots);
|
||||
$newquiz = $this->duplicate_quiz($this->course, $quiz);
|
||||
$this->assertEquals(7, $this->question_count($context->id));
|
||||
$context = \context_module::instance($newquiz->id);
|
||||
$newquizcontext = \context_module::instance($newquiz->id);
|
||||
// Count the questions in the quiz context.
|
||||
$this->assertEquals(0, $this->question_count($context->id));
|
||||
$this->assertEquals(0, $this->question_count($newquizcontext->id));
|
||||
|
||||
$this->verify_restored_quiz_layout(
|
||||
courseid: $this->course->id,
|
||||
expectedslots: [
|
||||
1 => $originalslots[1]->questionid,
|
||||
2 => $originalslots[2]->questionid,
|
||||
3 => [
|
||||
'filter' => $originalslots[3]->filtercondition['filter'],
|
||||
'randomquestionids' => array_keys($randomquestions[3]),
|
||||
],
|
||||
],
|
||||
expectequal: true,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user