mirror of
https://github.com/moodle/moodle.git
synced 2025-03-20 07:30:01 +01:00
MDL-71378 mod_qbank: Add question bank backup/restore and refactor tests
This commit is contained in:
parent
1db7518a7d
commit
966b264cf3
admin/settings
backup
moodle2
backup_activity_task.class.phpbackup_course_task.class.phpbackup_root_task.class.phprestore_stepslib.php
tests
tests
util
lang/en
lib
mod
qbank/backup/moodle2
quiz
accessrule/seb/tests
tests
question
bank/exporttoxml/tests
tests
type/essay/tests
tag/tests/event
@ -415,7 +415,6 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) {
|
||||
$temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_general_userscompletion', new lang_string('generaluserscompletion','backup'), new lang_string('configgeneraluserscompletion','backup'), array('value'=>1, 'locked'=>0)));
|
||||
$temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_general_logs', new lang_string('generallogs','backup'), new lang_string('configgenerallogs','backup'), array('value'=>0, 'locked'=>0)));
|
||||
$temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_general_histories', new lang_string('generalhistories','backup'), new lang_string('configgeneralhistories','backup'), array('value'=>0, 'locked'=>0)));
|
||||
$temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_general_questionbank', new lang_string('generalquestionbank','backup'), new lang_string('configgeneralquestionbank','backup'), array('value'=>1, 'locked'=>0)));
|
||||
$temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_general_groups',
|
||||
new lang_string('generalgroups', 'backup'), new lang_string('configgeneralgroups', 'backup'),
|
||||
array('value' => 1, 'locked' => 0)));
|
||||
@ -455,7 +454,6 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) {
|
||||
$temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_import_blocks', new lang_string('generalblocks','backup'), new lang_string('configgeneralblocks','backup'), array('value'=>1, 'locked'=>0)));
|
||||
$temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_import_filters', new lang_string('generalfilters','backup'), new lang_string('configgeneralfilters','backup'), array('value'=>1, 'locked'=>0)));
|
||||
$temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_import_calendarevents', new lang_string('generalcalendarevents','backup'), new lang_string('configgeneralcalendarevents','backup'), array('value'=>1, 'locked'=>0)));
|
||||
$temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_import_questionbank', new lang_string('generalquestionbank','backup'), new lang_string('configgeneralquestionbank','backup'), array('value'=>1, 'locked'=>0)));
|
||||
$temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_import_groups',
|
||||
new lang_string('generalgroups', 'backup'), new lang_string('configgeneralgroups', 'backup'),
|
||||
array('value' => 1, 'locked' => 0)));
|
||||
@ -585,7 +583,6 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) {
|
||||
$temp->add(new admin_setting_configcheckbox('backup/backup_auto_userscompletion', new lang_string('generaluserscompletion','backup'), new lang_string('configgeneraluserscompletion','backup'), 1));
|
||||
$temp->add(new admin_setting_configcheckbox('backup/backup_auto_logs', new lang_string('generallogs', 'backup'), new lang_string('configgenerallogs', 'backup'), 0));
|
||||
$temp->add(new admin_setting_configcheckbox('backup/backup_auto_histories', new lang_string('generalhistories','backup'), new lang_string('configgeneralhistories','backup'), 0));
|
||||
$temp->add(new admin_setting_configcheckbox('backup/backup_auto_questionbank', new lang_string('generalquestionbank','backup'), new lang_string('configgeneralquestionbank','backup'), 1));
|
||||
$temp->add(new admin_setting_configcheckbox('backup/backup_auto_groups', new lang_string('generalgroups', 'backup'),
|
||||
new lang_string('configgeneralgroups', 'backup'), 1));
|
||||
$temp->add(new admin_setting_configcheckbox('backup/backup_auto_competencies', new lang_string('generalcompetencies','backup'), new lang_string('configgeneralcompetencies','backup'), 1));
|
||||
|
@ -286,10 +286,6 @@ abstract class backup_activity_task extends backup_task {
|
||||
// All these are common settings to be shared by all activities.
|
||||
$activityincluded = $this->add_activity_included_setting($settingprefix);
|
||||
|
||||
if (question_module_uses_questions($this->modulename)) {
|
||||
$questionbank = $this->plan->get_setting('questionbank');
|
||||
$questionbank->add_dependency($activityincluded);
|
||||
}
|
||||
|
||||
$this->add_activity_userinfo_setting($settingprefix, $activityincluded);
|
||||
|
||||
|
@ -96,10 +96,6 @@ class backup_course_task extends backup_task {
|
||||
$this->add_step(new backup_annotate_groups_from_groupings('annotate_groups_from_groupings'));
|
||||
}
|
||||
|
||||
// Annotate the question_categories belonging to the course context (conditionally).
|
||||
if ($this->get_setting_value('questionbank')) {
|
||||
$this->add_step(new backup_calculate_question_categories('course_question_categories'));
|
||||
}
|
||||
|
||||
// Generate the roles file (optionally role assignments and always role overrides)
|
||||
$this->add_step(new backup_roles_structure_step('course_roles', 'roles.xml'));
|
||||
|
@ -166,10 +166,6 @@ class backup_root_task extends backup_task {
|
||||
// So let's define a dependency to prevent false expectations from our users.
|
||||
$activities->add_dependency($gradehistories);
|
||||
|
||||
// Define question bank inclusion setting.
|
||||
$questionbank = new backup_generic_setting('questionbank', base_setting::IS_BOOLEAN, true);
|
||||
$questionbank->set_ui(new backup_setting_ui_checkbox($questionbank, get_string('rootsettingquestionbank', 'backup')));
|
||||
$this->add_setting($questionbank);
|
||||
|
||||
$groups = new backup_groups_setting('groups', base_setting::IS_BOOLEAN, true);
|
||||
$groups->set_ui(new backup_setting_ui_checkbox($groups, get_string('rootsettinggroups', 'backup')));
|
||||
|
@ -5107,23 +5107,23 @@ class restore_create_categories_and_questions extends restore_structure_step {
|
||||
}
|
||||
$data->contextid = $mapping->parentitemid;
|
||||
|
||||
$context = \context::instance_by_id($data->contextid);
|
||||
|
||||
// Before 3.5, question categories could be created at top level.
|
||||
// From 3.5 onwards, all question categories should be a child of a special category called the "top" category.
|
||||
$restoretask = $this->get_task();
|
||||
$before35 = $restoretask->backup_release_compare('3.5', '<') || $restoretask->backup_version_compare(20180205, '<');
|
||||
|
||||
// We need a 'Top' question category for an activity module and activity modules are mapped to CONTEXT_COURSE and moved
|
||||
// to the correct module context in restore_move_module_questions_categories.
|
||||
// As we can't create a 'Top' category in CONTEXT_COURSE we'll make a default
|
||||
// qbank module and map it to that until they are created later.
|
||||
if (empty($mapping->info->parent) && $before35) {
|
||||
$top = question_get_top_category($data->contextid, true);
|
||||
$data->parent = $top->id;
|
||||
}
|
||||
|
||||
if (empty($data->parent)) {
|
||||
if (!$top = question_get_top_category($data->contextid)) {
|
||||
$top = question_get_top_category($data->contextid, true);
|
||||
$this->set_mapping('question_category_created', $oldid, $top->id, false, null, $data->contextid);
|
||||
}
|
||||
$this->set_mapping('question_category', $oldid, $top->id);
|
||||
} else {
|
||||
|
||||
if (!empty($data->parent)) {
|
||||
// Before 3.1, the 'stamp' field could be erroneously duplicated.
|
||||
// From 3.1 onwards, there's a unique index of (contextid, stamp).
|
||||
// If we encounter a duplicate in an old restore file, just generate a new stamp.
|
||||
@ -5388,20 +5388,19 @@ class restore_create_categories_and_questions extends restore_structure_step {
|
||||
|
||||
if (core_tag_tag::is_enabled('core_question', 'question')) {
|
||||
$tagname = $data->rawname;
|
||||
if (!empty($data->contextid) && $newcontextid = $this->get_mappingid('context', $data->contextid)) {
|
||||
$tagcontextid = $newcontextid;
|
||||
} else {
|
||||
// Get the category, so we can then later get the context.
|
||||
$categoryid = $this->get_new_parentid('question_category');
|
||||
if (empty($this->cachedcategory) || $this->cachedcategory->id != $categoryid) {
|
||||
$this->cachedcategory = $DB->get_record('question_categories', array('id' => $categoryid));
|
||||
}
|
||||
$tagcontextid = $this->cachedcategory->contextid;
|
||||
// Get the category, so we can then later get the context.
|
||||
$categoryid = $this->get_new_parentid('question_category');
|
||||
if (empty($this->cachedcategory) || $this->cachedcategory->id != $categoryid) {
|
||||
$this->cachedcategory = $DB->get_record('question_categories', ['id' => $categoryid]);
|
||||
}
|
||||
$tagcontextid = $this->cachedcategory->contextid;
|
||||
// Add the tag to the question.
|
||||
core_tag_tag::add_item_tag('core_question', 'question', $newquestion,
|
||||
context::instance_by_id($tagcontextid),
|
||||
$tagname);
|
||||
core_tag_tag::add_item_tag('core_question',
|
||||
'question',
|
||||
$newquestion,
|
||||
context::instance_by_id($tagcontextid),
|
||||
$tagname
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -5473,90 +5472,112 @@ class restore_move_module_questions_categories extends restore_execution_step {
|
||||
|
||||
$contexts = restore_dbops::restore_get_question_banks($this->get_restoreid(), CONTEXT_MODULE);
|
||||
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)) {
|
||||
continue;
|
||||
}
|
||||
// We have no target question bank so create a default bank for categories without a module to attach to.
|
||||
// This can occur when a quiz backup contains references to a question bank module,
|
||||
// that was not included in the backup and does not exist in the site being restored to.
|
||||
$course = get_course($this->get_courseid());
|
||||
$defaultqbank = core_question\local\bank\question_bank_helper::get_default_open_instance_system_type($course, true);
|
||||
$context = context_module::instance($defaultqbank->id);
|
||||
$newcontext = new stdClass();
|
||||
$newcontext->newitemid = $context->id;
|
||||
}
|
||||
// Only if context mapping exists (i.e. the module has been restored)
|
||||
if ($newcontext = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'context', $contextid)) {
|
||||
// Update all the qcats having their parentitemid set to the original contextid
|
||||
$modulecats = $DB->get_records_sql("SELECT itemid, newitemid, info
|
||||
FROM {backup_ids_temp}
|
||||
WHERE backupid = ?
|
||||
AND itemname = 'question_category'
|
||||
AND parentitemid = ?", array($this->get_restoreid(), $contextid));
|
||||
$top = question_get_top_category($newcontext->newitemid, true);
|
||||
$oldtopid = 0;
|
||||
$categoryids = [];
|
||||
foreach ($modulecats as $modulecat) {
|
||||
// Before 3.5, question categories could be created at top level.
|
||||
// From 3.5 onwards, all question categories should be a child of a special category called the "top" category.
|
||||
$info = backup_controller_dbops::decode_backup_temp_info($modulecat->info);
|
||||
if ($after35 && empty($info->parent)) {
|
||||
$oldtopid = $modulecat->newitemid;
|
||||
$modulecat->newitemid = $top->id;
|
||||
} else {
|
||||
$cat = new stdClass();
|
||||
$cat->id = $modulecat->newitemid;
|
||||
$cat->contextid = $newcontext->newitemid;
|
||||
if (empty($info->parent)) {
|
||||
$cat->parent = $top->id;
|
||||
}
|
||||
$DB->update_record('question_categories', $cat);
|
||||
$categoryids[] = (int)$cat->id;
|
||||
// Update all the qcats having their parentitemid set to the original contextid.
|
||||
$modulecats = $DB->get_records_sql("SELECT itemid, newitemid, info
|
||||
FROM {backup_ids_temp}
|
||||
WHERE backupid = ?
|
||||
AND itemname = 'question_category'
|
||||
AND parentitemid = ?",
|
||||
[$this->get_restoreid(), $contextid]
|
||||
);
|
||||
$top = question_get_top_category($newcontext->newitemid, true);
|
||||
$oldtopid = 0;
|
||||
$categoryids = [];
|
||||
foreach ($modulecats as $modulecat) {
|
||||
// Before 3.5, question categories could be created at top level.
|
||||
// From 3.5 onwards, all question categories should be a child of a special category called the "top" category.
|
||||
$info = backup_controller_dbops::decode_backup_temp_info($modulecat->info);
|
||||
if ($after35 && empty($info->parent)) {
|
||||
$oldtopid = $modulecat->newitemid;
|
||||
$modulecat->newitemid = $top->id;
|
||||
} else {
|
||||
$cat = new stdClass();
|
||||
$cat->id = $modulecat->newitemid;
|
||||
$cat->contextid = $newcontext->newitemid;
|
||||
if (empty($info->parent)) {
|
||||
$cat->parent = $top->id;
|
||||
}
|
||||
|
||||
// And set new contextid (and maybe update newitemid) also in question_category mapping (will be
|
||||
// used by {@link restore_create_question_files} later.
|
||||
restore_dbops::set_backup_ids_record($this->get_restoreid(), 'question_category', $modulecat->itemid,
|
||||
$modulecat->newitemid, $newcontext->newitemid);
|
||||
$DB->update_record('question_categories', $cat);
|
||||
$categoryids[] = (int) $cat->id;
|
||||
}
|
||||
|
||||
// Update the context id of any tags applied to any questions in these categories.
|
||||
if ($categoryids) {
|
||||
[$categorysql, $categoryidparams] = $DB->get_in_or_equal($categoryids, SQL_PARAMS_NAMED);
|
||||
$sqlupdate = "UPDATE {tag_instance}
|
||||
SET contextid = :newcontext
|
||||
WHERE component = :component
|
||||
AND itemtype = :itemtype
|
||||
AND itemid IN (SELECT DISTINCT bi.newitemid as questionid
|
||||
FROM {backup_ids_temp} bi
|
||||
JOIN {question} q ON q.id = bi.newitemid
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
WHERE bi.backupid = :backupid AND bi.itemname = 'question_created'
|
||||
AND qbe.questioncategoryid {$categorysql}) ";
|
||||
$params = [
|
||||
// And set new contextid (and maybe update newitemid) also in question_category mapping (will be
|
||||
// used by {@see restore_create_question_files} later.
|
||||
restore_dbops::set_backup_ids_record($this->get_restoreid(),
|
||||
'question_category',
|
||||
$modulecat->itemid,
|
||||
$modulecat->newitemid,
|
||||
$newcontext->newitemid
|
||||
);
|
||||
}
|
||||
|
||||
// Update the context id of any tags applied to any questions in these categories.
|
||||
if ($categoryids) {
|
||||
[$categorysql, $categoryidparams] = $DB->get_in_or_equal($categoryids, SQL_PARAMS_NAMED);
|
||||
$sqlupdate = "UPDATE {tag_instance}
|
||||
SET contextid = :newcontext
|
||||
WHERE component = :component
|
||||
AND itemtype = :itemtype
|
||||
AND itemid IN (SELECT DISTINCT bi.newitemid as questionid
|
||||
FROM {backup_ids_temp} bi
|
||||
JOIN {question} q ON q.id = bi.newitemid
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
WHERE bi.backupid = :backupid AND bi.itemname = 'question_created'
|
||||
AND qbe.questioncategoryid {$categorysql}) ";
|
||||
$params = [
|
||||
'newcontext' => $newcontext->newitemid,
|
||||
'component' => 'core_question',
|
||||
'itemtype' => 'question',
|
||||
'backupid' => $this->get_restoreid(),
|
||||
];
|
||||
$params += $categoryidparams;
|
||||
$DB->execute($sqlupdate, $params);
|
||||
];
|
||||
$params += $categoryidparams;
|
||||
$DB->execute($sqlupdate, $params);
|
||||
|
||||
// As explained in {@see restore_quiz_activity_structure_step::process_quiz_question_legacy_instance()}
|
||||
// question_set_references relating to random questions restored from old backups,
|
||||
// which pick from context_module question_categores, will have been restored with the wrong questioncontextid.
|
||||
// So, now, we need to find those, and updated the questioncontextid.
|
||||
// We can only find them by picking apart the filter conditions, and seeign which categories they refer to.
|
||||
// As explained in {@see restore_quiz_activity_structure_step::process_quiz_question_legacy_instance()}
|
||||
// question_set_references relating to random questions restored from old backups,
|
||||
// which pick from context_module question_categores, will have been restored with the wrong questioncontextid.
|
||||
// So, now, we need to find those, and updated the questioncontextid.
|
||||
// We can only find them by picking apart the filter conditions, and seeign which categories they refer to.
|
||||
|
||||
// We need to check all the question_set_references belonging to this context_module.
|
||||
$references = $DB->get_records('question_set_references', ['usingcontextid' => $newcontext->newitemid]);
|
||||
foreach ($references as $reference) {
|
||||
$filtercondition = json_decode($reference->filtercondition);
|
||||
if (!empty($filtercondition->questioncategoryid) &&
|
||||
in_array($filtercondition->questioncategoryid, $categoryids)) {
|
||||
// This is one of ours, update the questionscontextid.
|
||||
$DB->set_field('question_set_references',
|
||||
'questionscontextid', $newcontext->newitemid,
|
||||
['id' => $reference->id]);
|
||||
}
|
||||
// We need to check all the question_set_references belonging to this context_module.
|
||||
$references = $DB->get_records('question_set_references', ['usingcontextid' => $newcontext->newitemid]);
|
||||
foreach ($references as $reference) {
|
||||
$filtercondition = json_decode($reference->filtercondition);
|
||||
if (!empty($filtercondition->questioncategoryid) &&
|
||||
in_array($filtercondition->questioncategoryid, $categoryids)) {
|
||||
// This is one of ours, update the questionscontextid.
|
||||
$DB->set_field('question_set_references',
|
||||
'questionscontextid', $newcontext->newitemid,
|
||||
['id' => $reference->id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now set the parent id for the question categories that were in the top category in the course context
|
||||
// and have been moved now.
|
||||
if ($oldtopid) {
|
||||
$DB->set_field('question_categories', 'parent', $top->id,
|
||||
array('contextid' => $newcontext->newitemid, 'parent' => $oldtopid));
|
||||
}
|
||||
// Now set the parent id for the question categories that were in the top category in the course context
|
||||
// and have been moved now.
|
||||
if ($oldtopid) {
|
||||
$DB->set_field('question_categories',
|
||||
'parent',
|
||||
$top->id,
|
||||
['contextid' => $newcontext->newitemid, 'parent' => $oldtopid]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -141,7 +141,7 @@ final class moodle2_test extends \advanced_testcase {
|
||||
backup::TARGET_NEW_COURSE);
|
||||
$thrown = null;
|
||||
try {
|
||||
$this->assertTrue($rc->execute_precheck());
|
||||
$rc->execute_precheck();
|
||||
$rc->execute_plan();
|
||||
$rc->destroy();
|
||||
} catch (Exception $e) {
|
||||
@ -155,6 +155,11 @@ final class moodle2_test extends \advanced_testcase {
|
||||
|
||||
$this->assertNull($thrown);
|
||||
|
||||
// Backup contained a question category in a deprecated context.
|
||||
$results = $rc->get_precheck_results();
|
||||
$this->assertCount(1, $results['warnings']);
|
||||
$this->assertStringStartsWith('The questions category', $results['warnings'][0]);
|
||||
|
||||
// Get information about the resulting course and check that it is set
|
||||
// up correctly.
|
||||
$modinfo = get_fast_modinfo($newcourseid);
|
||||
@ -1040,13 +1045,24 @@ final class moodle2_test extends \advanced_testcase {
|
||||
backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
|
||||
backup::TARGET_NEW_COURSE);
|
||||
|
||||
$this->assertTrue($rc->execute_precheck());
|
||||
$rc->execute_precheck();
|
||||
$rc->execute_plan();
|
||||
$rc->destroy();
|
||||
|
||||
// Backup contained question category(s) in a deprecated context.
|
||||
$expectedwarnings = $backupid === 'question_category_34_format' ? 1 : 2;
|
||||
$results = $rc->get_precheck_results();
|
||||
$this->assertCount($expectedwarnings, $results['warnings']);
|
||||
for ($i = 0; $i < $expectedwarnings; $i++) {
|
||||
$this->assertStringStartsWith('The questions category', $results['warnings'][$i]);
|
||||
}
|
||||
|
||||
// Get information about the resulting course and check that it is set up correctly.
|
||||
$modinfo = get_fast_modinfo($newcourseid);
|
||||
$quizzes = array_values($modinfo->get_instances_of('quiz'));
|
||||
$qbanks = $modinfo->get_instances_of('qbank');
|
||||
$this->assertCount(1, $qbanks);
|
||||
$qbank = reset($qbanks);
|
||||
$contexts = $quizzes[0]->context->get_parent_contexts(true);
|
||||
|
||||
$topcategorycount = [];
|
||||
@ -1055,6 +1071,11 @@ final class moodle2_test extends \advanced_testcase {
|
||||
|
||||
// Make sure all question categories that were inside the backup file were restored correctly.
|
||||
if ($context->contextlevel == CONTEXT_COURSE) {
|
||||
// Course context categories are deprecated and now get transferred to a qbank instance on the course
|
||||
// at point of restore.
|
||||
$cats = $DB->get_records('question_categories',
|
||||
['contextid' => $qbank->context->id], 'parent', 'id, name, parent'
|
||||
);
|
||||
$this->assertEquals(['top', 'Default for C101'], array_column($cats, 'name'));
|
||||
} else if ($context->contextlevel == CONTEXT_MODULE) {
|
||||
$this->assertEquals(['top', 'Default for Q1'], array_column($cats, 'name'));
|
||||
|
@ -49,11 +49,12 @@ class quiz_restore_decode_links_test extends \advanced_testcase {
|
||||
array('createsections' => true));
|
||||
$quiz = $generator->create_module('quiz', array(
|
||||
'course' => $course->id));
|
||||
$qbank = $this->getDataGenerator()->create_module('qbank', ['course' => $course->id]);
|
||||
|
||||
// Create questions.
|
||||
|
||||
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
$context = \context_course::instance($course->id);
|
||||
$context = \context_module::instance($qbank->cmid);
|
||||
$cat = $questiongenerator->create_question_category(array('contextid' => $context->id));
|
||||
$question = $questiongenerator->create_question('multichoice', null, array('category' => $cat->id));
|
||||
|
||||
|
@ -569,7 +569,6 @@ abstract class backup_controller_dbops extends backup_dbops {
|
||||
'backup_general_userscompletion' => 'userscompletion',
|
||||
'backup_general_logs' => 'logs',
|
||||
'backup_general_histories' => 'grade_histories',
|
||||
'backup_general_questionbank' => 'questionbank',
|
||||
'backup_general_groups' => 'groups',
|
||||
'backup_general_competencies' => 'competencies',
|
||||
'backup_general_contentbankcontent' => 'contentbankcontent',
|
||||
@ -586,7 +585,6 @@ abstract class backup_controller_dbops extends backup_dbops {
|
||||
'backup_import_filters' => 'filters',
|
||||
'backup_import_calendarevents' => 'calendarevents',
|
||||
'backup_import_permissions' => 'permissions',
|
||||
'backup_import_questionbank' => 'questionbank',
|
||||
'backup_import_groups' => 'groups',
|
||||
'backup_import_competencies' => 'competencies',
|
||||
'backup_import_contentbankcontent' => 'contentbankcontent',
|
||||
@ -600,7 +598,6 @@ abstract class backup_controller_dbops extends backup_dbops {
|
||||
'activities',
|
||||
'blocks',
|
||||
'filters',
|
||||
'questionbank'
|
||||
);
|
||||
self::force_enable_settings($controller, $settings);
|
||||
}
|
||||
@ -620,7 +617,6 @@ abstract class backup_controller_dbops extends backup_dbops {
|
||||
'backup_auto_userscompletion' => 'userscompletion',
|
||||
'backup_auto_logs' => 'logs',
|
||||
'backup_auto_histories' => 'grade_histories',
|
||||
'backup_auto_questionbank' => 'questionbank',
|
||||
'backup_auto_groups' => 'groups',
|
||||
'backup_auto_competencies' => 'competencies',
|
||||
'backup_auto_contentbankcontent' => 'contentbankcontent',
|
||||
|
@ -156,7 +156,6 @@ abstract class restore_controller_dbops extends restore_dbops {
|
||||
'restore_general_userscompletion' => 'userscompletion',
|
||||
'restore_general_logs' => 'logs',
|
||||
'restore_general_histories' => 'grade_histories',
|
||||
'restore_general_questionbank' => 'questionbank',
|
||||
'restore_general_groups' => 'groups',
|
||||
'restore_general_competencies' => 'competencies',
|
||||
'restore_general_contentbankcontent' => 'contentbankcontent',
|
||||
@ -195,7 +194,6 @@ abstract class restore_controller_dbops extends restore_dbops {
|
||||
'activities',
|
||||
'blocks',
|
||||
'filters',
|
||||
'questionbank'
|
||||
);
|
||||
self::force_enable_settings($controller, $settings);
|
||||
};
|
||||
|
@ -538,10 +538,11 @@ abstract class restore_dbops {
|
||||
* the target contexts where each bank will be restored and returning
|
||||
* warnings/errors as needed.
|
||||
*
|
||||
* Some contextlevels (system, coursecat), will delegate process to
|
||||
* course level if any problem is found (lack of permissions, non-matching
|
||||
* target context...). Other contextlevels (course, module) will
|
||||
* cause return error if some problem is found.
|
||||
* Question categories at CONTEXT_SYSTEM, CONTEXT_COURSE, and CONTEXT_COURSECAT
|
||||
* are now deprecated, but we still have to account for them in backup files
|
||||
* made with pre-deprecated code. As such, any categories in backup files that used
|
||||
* to target these contexts will now be attached to a 'fallback' qbank
|
||||
* instance on the course being restored.
|
||||
*
|
||||
* At the end, if no errors were found, all the categories in backup_temp_ids
|
||||
* will be pointing (parentitemid) to the target context where they must be
|
||||
@ -570,13 +571,8 @@ abstract class restore_dbops {
|
||||
global $DB;
|
||||
|
||||
// To return any errors and warnings found
|
||||
$errors = array();
|
||||
$warnings = array();
|
||||
|
||||
// Specify which fallbacks must be performed
|
||||
$fallbacks = array(
|
||||
CONTEXT_SYSTEM => CONTEXT_COURSE,
|
||||
CONTEXT_COURSECAT => CONTEXT_COURSE);
|
||||
$errors = [];
|
||||
$warnings = [];
|
||||
|
||||
/** @var restore_controller $rc */
|
||||
$rc = restore_controller_dbops::load_controller($restoreid);
|
||||
@ -584,24 +580,22 @@ abstract class restore_dbops {
|
||||
$after35 = $plan->backup_release_compare('3.5', '>=') && $plan->backup_version_compare(20180205, '>');
|
||||
$rc->destroy(); // Always need to destroy.
|
||||
|
||||
// For any contextlevel, follow this process logic:
|
||||
//
|
||||
// 0) Iterate over each context (qbank)
|
||||
// 1) Iterate over each qcat in the context, matching by stamp for the found target context
|
||||
// 2a) No match, check if user can create qcat and q
|
||||
// 3a) User can, mark the qcat and all dependent qs to be created in that target context
|
||||
// 3b) User cannot, check if we are in some contextlevel with fallback
|
||||
// 4a) There is fallback, move ALL the qcats to fallback, warn. End qcat loop
|
||||
// 4b) No fallback, error. End qcat loop.
|
||||
// 2b) Match, mark qcat to be mapped and iterate over each q, matching by stamp and version
|
||||
// 5a) No match, check if user can add q
|
||||
// 6a) User can, mark the q to be created
|
||||
// 6b) User cannot, check if we are in some contextlevel with fallback
|
||||
// 7a) There is fallback, move ALL the qcats to fallback, warn. End qcat loop
|
||||
// 7b) No fallback, error. End qcat loop
|
||||
// 5b) Random question, must always create new.
|
||||
// 5c) Match, mark q to be mapped
|
||||
// 8) Check if backup is from Moodle >= 3.5 and error if more than one top-level category in the context.
|
||||
/*
|
||||
For any contextlevel, follow this process logic:
|
||||
|
||||
0) Iterate over each context (qbank)
|
||||
1) Iterate over each qcat in the context, matching by stamp for the found target context
|
||||
2a) No match, check if user can create qcat and q
|
||||
3a) User can, mark the qcat and all dependent qs to be created in that target context
|
||||
3b) User cannot. Move ALL the qcats to a default qbank instance, warn. End qcat loop
|
||||
2b) Match, mark qcat to be mapped and iterate over each q, matching by stamp and version
|
||||
4a) No match, check if user can add q
|
||||
5a) User can, mark the q to be created
|
||||
5b) User cannot. Move ALL the qcats to a default qbank instance, warn. End qcat loop
|
||||
4b) Random question, must always create new.
|
||||
4c) Match, mark q to be mapped
|
||||
6) Check if backup is from Moodle >= 3.5 and error if more than one top-level category in the context.
|
||||
*/
|
||||
|
||||
// Get all the contexts (question banks) in restore for the given contextlevel
|
||||
$contexts = self::restore_get_question_banks($restoreid, $contextlevel);
|
||||
@ -610,7 +604,7 @@ abstract class restore_dbops {
|
||||
foreach ($contexts as $contextid => $contextlevel) {
|
||||
// Init some perms
|
||||
$canmanagecategory = false;
|
||||
$canadd = false;
|
||||
$canadd = false;
|
||||
// Top-level category counter.
|
||||
$topcats = 0;
|
||||
// get categories in context (bank)
|
||||
@ -619,7 +613,7 @@ abstract class restore_dbops {
|
||||
// cache permissions if $targetcontext is found
|
||||
if ($targetcontext = self::restore_find_best_target_context($categories, $courseid, $contextlevel)) {
|
||||
$canmanagecategory = has_capability('moodle/question:managecategory', $targetcontext, $userid);
|
||||
$canadd = has_capability('moodle/question:add', $targetcontext, $userid);
|
||||
$canadd = has_capability('moodle/question:add', $targetcontext, $userid);
|
||||
}
|
||||
// 1) Iterate over each qcat in the context, matching by stamp for the found target context
|
||||
foreach ($categories as $category) {
|
||||
@ -629,9 +623,10 @@ abstract class restore_dbops {
|
||||
|
||||
$matchcat = false;
|
||||
if ($targetcontext) {
|
||||
$matchcat = $DB->get_record('question_categories', array(
|
||||
'contextid' => $targetcontext->id,
|
||||
'stamp' => $category->stamp));
|
||||
$matchcat = $DB->get_record('question_categories', [
|
||||
'contextid' => $targetcontext->id,
|
||||
'stamp' => $category->stamp,
|
||||
]);
|
||||
}
|
||||
// 2a) No match, check if user can create qcat and q
|
||||
if (!$matchcat) {
|
||||
@ -646,26 +641,29 @@ abstract class restore_dbops {
|
||||
}
|
||||
self::set_backup_ids_record($restoreid, 'question_category', $category->id, 0, $parentitemid);
|
||||
// Nothing else to mark, newitemid = 0 means create
|
||||
|
||||
// 3b) User cannot, check if we are in some contextlevel with fallback
|
||||
} else {
|
||||
// 4a) There is fallback, move ALL the qcats to fallback, warn. End qcat loop
|
||||
if (array_key_exists($contextlevel, $fallbacks)) {
|
||||
foreach ($categories as $movedcat) {
|
||||
$movedcat->contextlevel = $fallbacks[$contextlevel];
|
||||
self::set_backup_ids_record($restoreid, 'question_category', $movedcat->id, 0, $contextid, $movedcat);
|
||||
// Warn about the performed fallback
|
||||
$warnings[] = get_string('qcategory2coursefallback', 'backup', $movedcat);
|
||||
}
|
||||
|
||||
// 4b) No fallback, error. End qcat loop.
|
||||
} else {
|
||||
$errors[] = get_string('qcategorycannotberestored', 'backup', $category);
|
||||
// 3b) User cannot. Move ALL the qcats to the fallback i.e. a default qbank instance, warn. End qcat loop.
|
||||
$course = get_course($courseid);
|
||||
$course->fullname = get_string('courserestore', 'question');
|
||||
$module =
|
||||
core_question\local\bank\question_bank_helper::get_default_open_instance_system_type($course, true);
|
||||
$fallbackcontext = $module->context;
|
||||
foreach ($categories as $movedcat) {
|
||||
$movedcat->contextlevel = $contextlevel;
|
||||
self::set_backup_ids_record($restoreid,
|
||||
'question_category',
|
||||
$movedcat->id,
|
||||
0,
|
||||
$fallbackcontext->id,
|
||||
$movedcat
|
||||
);
|
||||
// Warn about the performed fallback.
|
||||
$warnings[] = get_string('qcategory2coursefallback', 'backup', $movedcat);
|
||||
}
|
||||
break; // out from qcat loop (both 4a and 4b), we have decided about ALL categories in context (bank)
|
||||
break; // Out from qcat loop (both 3a and 3b), we have decided about ALL categories in context (bank).
|
||||
}
|
||||
|
||||
// 2b) Match, mark qcat to be mapped and iterate over each q, matching by stamp and version
|
||||
// 2b) Match, mark qcat to be mapped and iterate over each q, matching by stamp and version
|
||||
} else {
|
||||
self::set_backup_ids_record($restoreid, 'question_category', $category->id, $matchcat->id, $targetcontext->id);
|
||||
$questions = self::restore_get_questions($restoreid, $category->id);
|
||||
@ -687,35 +685,42 @@ abstract class restore_dbops {
|
||||
} else {
|
||||
$matchqid = false;
|
||||
}
|
||||
// 5a) No match, check if user can add q
|
||||
// 4a) No match, check if user can add q
|
||||
if (!$matchqid) {
|
||||
// 6a) User can, mark the q to be created
|
||||
// 5a) User can, mark the q to be created
|
||||
if ($canadd) {
|
||||
// Nothing to mark, newitemid means create
|
||||
|
||||
// 6b) User cannot, check if we are in some contextlevel with fallback
|
||||
} else {
|
||||
// 7a) There is fallback, move ALL the qcats to fallback, warn. End qcat loo
|
||||
if (array_key_exists($contextlevel, $fallbacks)) {
|
||||
foreach ($categories as $movedcat) {
|
||||
$movedcat->contextlevel = $fallbacks[$contextlevel];
|
||||
self::set_backup_ids_record($restoreid, 'question_category', $movedcat->id, 0, $contextid, $movedcat);
|
||||
// Warn about the performed fallback
|
||||
$warnings[] = get_string('question2coursefallback', 'backup', $movedcat);
|
||||
}
|
||||
|
||||
// 7b) No fallback, error. End qcat loop
|
||||
} else {
|
||||
$errors[] = get_string('questioncannotberestored', 'backup', $question);
|
||||
// 5b) User cannot.
|
||||
// Move ALL the qcats to the fallback i.e. a default qbank instance, warn. End qcat loop.
|
||||
$course = get_course($courseid);
|
||||
$course->fullname = get_string('courserestore', 'question');
|
||||
$module = core_question\local\bank\question_bank_helper::get_default_open_instance_system_type(
|
||||
$course,
|
||||
true
|
||||
);
|
||||
$fallbackcontext = $module->context;
|
||||
foreach ($categories as $movedcat) {
|
||||
$movedcat->contextlevel = $contextlevel;
|
||||
self::set_backup_ids_record($restoreid,
|
||||
'question_category',
|
||||
$movedcat->id,
|
||||
0,
|
||||
$fallbackcontext->id,
|
||||
$movedcat
|
||||
);
|
||||
// Warn about the performed fallback.
|
||||
$warnings[] = get_string('question2coursefallback', 'backup', $movedcat);
|
||||
}
|
||||
break 2; // out from qcat loop (both 7a and 7b), we have decided about ALL categories in context (bank)
|
||||
// Out from qcat loop (both 5a and 5b), we have decided about ALL categories in context (bank).
|
||||
break 2;
|
||||
}
|
||||
|
||||
// 5b) Random questions must always be newly created.
|
||||
// 4b) Random questions must always be newly created.
|
||||
} else if ($question->qtype == 'random') {
|
||||
// Nothing to mark, newitemid means create
|
||||
|
||||
// 5c) Match, mark q to be mapped.
|
||||
// 4c) Match, mark q to be mapped.
|
||||
} else {
|
||||
self::set_backup_ids_record($restoreid, 'question', $question->id, $matchqid);
|
||||
}
|
||||
@ -723,14 +728,14 @@ abstract class restore_dbops {
|
||||
}
|
||||
}
|
||||
|
||||
// 8) Check if backup is made on Moodle >= 3.5 and there are more than one top-level category in the context.
|
||||
// 6) Check if backup is made on Moodle >= 3.5 and there are more than one top-level category in the context.
|
||||
if ($after35 && $topcats > 1) {
|
||||
$errors[] = get_string('restoremultipletopcats', 'question', $contextid);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return array($errors, $warnings);
|
||||
return [$errors, $warnings];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -798,80 +803,57 @@ abstract class restore_dbops {
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the best context found to restore one collection of qcats,
|
||||
* al them belonging to the same context (question bank), returning the
|
||||
* target context found (object) or false
|
||||
* Calculates the best existing context to restore one collection of qcats.
|
||||
* Uses the backup category stamp to match the target category stamp
|
||||
* and categories must all belong to the same context (question bank).
|
||||
*
|
||||
* @param array $categories categories to find target context for
|
||||
* @param int $courseid course to restore to
|
||||
* @param int $contextlevel contextlevel to search for the target context
|
||||
* @return bool|\core\context target context or false if no target context found
|
||||
*/
|
||||
public static function restore_find_best_target_context($categories, $courseid, $contextlevel) {
|
||||
global $DB;
|
||||
|
||||
$targetcontext = false;
|
||||
|
||||
// Depending of $contextlevel, we perform different actions
|
||||
switch ($contextlevel) {
|
||||
// For system is easy, the best context is the system context
|
||||
case CONTEXT_SYSTEM:
|
||||
$targetcontext = context_system::instance();
|
||||
break;
|
||||
// If context module we need to find any existing module instances with categories matching the category stamps
|
||||
// from the backup. If multiple matches are found, that means that there is some annoying
|
||||
// qbank "fragmentation" in the categories, so we'll fall back
|
||||
// to creating a qbank instance at course level and putting the categories there.
|
||||
if ($contextlevel == CONTEXT_MODULE) {
|
||||
$stamps = [];
|
||||
foreach ($categories as $category) {
|
||||
$stamps[] = $category->stamp;
|
||||
}
|
||||
$modinfo = get_fast_modinfo($courseid);
|
||||
|
||||
// For coursecat, we are going to look for stamps in all the
|
||||
// course categories between CONTEXT_SYSTEM and CONTEXT_COURSE
|
||||
// (i.e. in all the course categories in the path)
|
||||
//
|
||||
// And only will return one "best" target context if all the
|
||||
// matches belong to ONE and ONLY ONE context. If multiple
|
||||
// matches are found, that means that there is some annoying
|
||||
// qbank "fragmentation" in the categories, so we'll fallback
|
||||
// to create the qbank at course level
|
||||
case CONTEXT_COURSECAT:
|
||||
// Build the array of stamps we are going to match
|
||||
$stamps = array();
|
||||
foreach ($categories as $category) {
|
||||
$stamps[] = $category->stamp;
|
||||
}
|
||||
$contexts = array();
|
||||
// Build the array of contexts we are going to look
|
||||
$systemctx = context_system::instance();
|
||||
$coursectx = context_course::instance($courseid);
|
||||
$parentctxs = $coursectx->get_parent_context_ids();
|
||||
foreach ($parentctxs as $parentctx) {
|
||||
// Exclude system context
|
||||
if ($parentctx == $systemctx->id) {
|
||||
continue;
|
||||
}
|
||||
$contexts[] = $parentctx;
|
||||
}
|
||||
if (!empty($stamps) && !empty($contexts)) {
|
||||
// Prepare the query
|
||||
list($stamp_sql, $stamp_params) = $DB->get_in_or_equal($stamps);
|
||||
list($context_sql, $context_params) = $DB->get_in_or_equal($contexts);
|
||||
$sql = "SELECT DISTINCT contextid
|
||||
FROM {question_categories}
|
||||
WHERE stamp $stamp_sql
|
||||
AND contextid $context_sql";
|
||||
$params = array_merge($stamp_params, $context_params);
|
||||
$matchingcontexts = $DB->get_records_sql($sql, $params);
|
||||
// Only if ONE and ONLY ONE context is found, use it as valid target
|
||||
if (count($matchingcontexts) == 1) {
|
||||
$targetcontext = context::instance_by_id(reset($matchingcontexts)->contextid);
|
||||
}
|
||||
}
|
||||
break;
|
||||
// Get contextids of modules from the course that support publishing questions.
|
||||
$supportedcontextids = [];
|
||||
foreach ($modinfo->get_cms() as $cm) {
|
||||
if (plugin_supports('mod', $cm->modname, FEATURE_PUBLISHES_QUESTIONS, false)) {
|
||||
$supportedcontextids[] = $cm->context->id;
|
||||
}
|
||||
}
|
||||
|
||||
// For course is easy, the best context is the course context
|
||||
case CONTEXT_COURSE:
|
||||
$targetcontext = context_course::instance($courseid);
|
||||
break;
|
||||
|
||||
// For module is easy, there is not best context, as far as the
|
||||
// activity hasn't been created yet. So we return context course
|
||||
// for them, so permission checks and friends will work. Note this
|
||||
// case is handled by {@link prechek_precheck_qbanks_by_level}
|
||||
// in an special way
|
||||
case CONTEXT_MODULE:
|
||||
$targetcontext = context_course::instance($courseid);
|
||||
break;
|
||||
if (!empty($stamps) && !empty($supportedcontextids)) {
|
||||
[$stampsql, $stampparams] = $DB->get_in_or_equal($stamps);
|
||||
[$contextsql, $contextparams] = $DB->get_in_or_equal($supportedcontextids);
|
||||
$sql = "SELECT DISTINCT contextid
|
||||
FROM {question_categories}
|
||||
WHERE stamp {$stampsql}
|
||||
AND contextid {$contextsql}";
|
||||
$params = array_merge($stampparams, $contextparams);
|
||||
$matchingcontexts = $DB->get_records_sql($sql, $params);
|
||||
// Only if ONE and ONLY ONE context is found, use it as valid target.
|
||||
if (count($matchingcontexts) === 1) {
|
||||
$targetcontext = context::instance_by_id(reset($matchingcontexts)->contextid);
|
||||
}
|
||||
}
|
||||
// We don't have a target so set as course context until the module is created and then assign to the module context.
|
||||
$targetcontext = $targetcontext ?: context_course::instance($courseid);
|
||||
}
|
||||
|
||||
return $targetcontext;
|
||||
}
|
||||
|
||||
|
@ -275,7 +275,6 @@ Feature: Restore Moodle 2 course backups
|
||||
| Initial | Include files | 0 |
|
||||
| Initial | Include filters | 0 |
|
||||
| Initial | Include calendar events | 0 |
|
||||
| Initial | Include question bank | 0 |
|
||||
| Initial | Include groups and groupings | 0 |
|
||||
| Initial | Include competencies | 0 |
|
||||
| Initial | Include custom fields | 0 |
|
||||
|
@ -134,7 +134,6 @@ $string['configgeneralfiles'] = 'Sets the default for including files in a backu
|
||||
$string['configgeneralfilters'] = 'Sets the default for including filters in a backup.';
|
||||
$string['configgeneralhistories'] = 'Sets the default for including user history within a backup.';
|
||||
$string['configgenerallogs'] = 'If enabled logs will be included in backups by default.';
|
||||
$string['configgeneralquestionbank'] = 'If enabled the question bank will be included in backups by default. PLEASE NOTE: Disabling this setting will disable the backup of activities which use the question bank, such as the quiz.';
|
||||
$string['configgeneralgroups'] = 'Sets the default for including groups and groupings in a backup.';
|
||||
$string['configgeneralroleassignments'] = 'If enabled by default roles assignments will also be backed up.';
|
||||
$string['configgeneralpermissions'] = 'If enabled the role permissions will be imported. This may override existing permissions for enrolled users.';
|
||||
@ -236,7 +235,6 @@ $string['generalhistories'] = 'Include histories';
|
||||
$string['generalgradehistories'] = 'Include histories';
|
||||
$string['generallegacyfiles'] = 'Include legacy course files';
|
||||
$string['generallogs'] = 'Include logs';
|
||||
$string['generalquestionbank'] = 'Include question bank';
|
||||
$string['generalgroups'] = 'Include groups and groupings';
|
||||
$string['generalrestoredefaults'] = 'General restore defaults';
|
||||
$string['mergerestoredefaults'] = 'Restore defaults when merging into another course';
|
||||
@ -314,9 +312,9 @@ $string['privacy:metadata:backup_controllers:operation'] = 'The operation that w
|
||||
$string['privacy:metadata:backup_controllers:timecreated'] = 'The time when the action was created';
|
||||
$string['privacy:metadata:backup_controllers:timemodified'] = 'The time when the action was modified';
|
||||
$string['privacy:metadata:backup_controllers:type'] = 'The type of the item being operated on, eg. activity.';
|
||||
$string['qcategory2coursefallback'] = 'The questions category "{$a->name}", originally at system/course category context in backup file, will be created at course context by restore';
|
||||
$string['qcategory2coursefallback'] = 'The questions category "{$a->name}", originally at system|course|course_category context in backup file, will be created at a question bank module context by restore';
|
||||
$string['qcategorycannotberestored'] = 'The questions category "{$a->name}" cannot be created by restore';
|
||||
$string['question2coursefallback'] = 'The questions category "{$a->name}", originally at system/course category context in backup file, will be created at course context by restore';
|
||||
$string['question2coursefallback'] = 'The questions category "{$a->name}", originally at system|course|course_category in backup file, will be created at a question bank module context by restore';
|
||||
$string['questioncannotberestored'] = 'The questions "{$a->name}" cannot be created by restore';
|
||||
$string['restoreactivity'] = 'Restore activity';
|
||||
$string['restorecourse'] = 'Restore course';
|
||||
@ -437,3 +435,7 @@ $string['recyclebin_desc'] = 'Note that these settings will also be used for the
|
||||
// Deprecated since Moodle 4.4.
|
||||
$string['copycourseheading'] = 'Copy a course';
|
||||
$string['backupcourse'] = 'Backup course: {$a}';
|
||||
|
||||
// Deprecated since Moodle 5.0.
|
||||
$string['configgeneralquestionbank'] = 'If enabled the question bank will be included in backups by default. PLEASE NOTE: Disabling this setting will disable the backup of activities which use the question bank, such as the quiz.';
|
||||
$string['generalquestionbank'] = 'Include question bank';
|
||||
|
@ -136,3 +136,7 @@ filterfirstactive,core_grades
|
||||
filterlastactive,core_grades
|
||||
noreplybouncemessage,core
|
||||
noreplybouncesubject,core
|
||||
configgeneralquestionbank,core_backup
|
||||
generalquestionbank,core_backup
|
||||
cannotdeletecategoryquestions,core_error
|
||||
errordeletingquestionsfromcategory,core_question
|
||||
|
@ -368,6 +368,7 @@ $string['contexterror'] = 'You shouldn\'t have got here if you\'re not moving a
|
||||
$string['correct'] = 'Correct';
|
||||
$string['correctfeedback'] = 'For any correct response';
|
||||
$string['correctfeedbackdefault'] = 'Your answer is correct.';
|
||||
$string['courserestore'] = 'Course restore';
|
||||
$string['decimalplacesingrades'] = 'Decimal places in grades';
|
||||
$string['defaultmark'] = 'Default mark';
|
||||
$string['errorsavingflags'] = 'Error saving the flag state.';
|
||||
|
@ -1461,6 +1461,14 @@ function xmldb_main_upgrade($oldversion) {
|
||||
// Main savepoint reached.
|
||||
upgrade_main_savepoint(true, 2024110400.00);
|
||||
}
|
||||
if ($oldversion < 2024110800.00) {
|
||||
// Delete settings that were removed from code.
|
||||
$settings = ['backup_general_questionbank', 'backup_import_questionbank', 'backup_auto_questionbank'];
|
||||
array_walk($settings, static fn($setting) => unset_config($setting, 'backup'));
|
||||
|
||||
// Main savepoint reached.
|
||||
upgrade_main_savepoint(true, 2024110800.00);
|
||||
}
|
||||
|
||||
if ($oldversion < 2024110800.02) {
|
||||
// Changing type of field value on table user_preferences to text.
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
namespace core;
|
||||
|
||||
use core_question\local\bank\question_bank_helper;
|
||||
use question_bank;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
@ -162,83 +163,65 @@ class questionlib_test extends \advanced_testcase {
|
||||
// Set to admin user.
|
||||
$this->setAdminUser();
|
||||
|
||||
// Create two course categories - we are going to delete one of these later and will expect
|
||||
// all the questions belonging to the course in the deleted category to be moved.
|
||||
// Create 2 qbank instances - we are going to delete one of these later and will expect
|
||||
// all the questions belonging to the deleted module to be moved.
|
||||
$coursecat1 = $this->getDataGenerator()->create_category();
|
||||
$course1 = $this->getDataGenerator()->create_course(['category' => $coursecat1->id]);
|
||||
$modqbank1 = $this->getDataGenerator()->create_module('qbank', ['course' => $course1->id]);
|
||||
$coursecat2 = $this->getDataGenerator()->create_category();
|
||||
$course2 = $this->getDataGenerator()->create_course(['category' => $coursecat2->id]);
|
||||
$modqbank2 = $this->getDataGenerator()->create_module('qbank', ['course' => $course2->id]);
|
||||
|
||||
// Create a couple of categories and questions.
|
||||
$context1 = \context_coursecat::instance($coursecat1->id);
|
||||
$context2 = \context_coursecat::instance($coursecat2->id);
|
||||
$context1 = \context_module::instance($modqbank1->cmid);
|
||||
$context2 = \context_module::instance($modqbank2->cmid);
|
||||
/** @var \core_question_generator $questiongenerator */
|
||||
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
$questioncat1 = $questiongenerator->create_question_category(array('contextid' =>
|
||||
$context1->id));
|
||||
$questioncat2 = $questiongenerator->create_question_category(array('contextid' =>
|
||||
$context2->id));
|
||||
$question1 = $questiongenerator->create_question('shortanswer', null, array('category' => $questioncat1->id));
|
||||
$question2 = $questiongenerator->create_question('shortanswer', null, array('category' => $questioncat1->id));
|
||||
$question3 = $questiongenerator->create_question('shortanswer', null, array('category' => $questioncat2->id));
|
||||
$question4 = $questiongenerator->create_question('shortanswer', null, array('category' => $questioncat2->id));
|
||||
$questioncat1 = $questiongenerator->create_question_category(['contextid' =>
|
||||
$context1->id]);
|
||||
$questioncat2 = $questiongenerator->create_question_category(['contextid' =>
|
||||
$context2->id]);
|
||||
$question1 = $questiongenerator->create_question('shortanswer', null, ['category' => $questioncat1->id]);
|
||||
$question2 = $questiongenerator->create_question('shortanswer', null, ['category' => $questioncat1->id]);
|
||||
$question3 = $questiongenerator->create_question('shortanswer', null, ['category' => $questioncat2->id]);
|
||||
$question4 = $questiongenerator->create_question('shortanswer', null, ['category' => $questioncat2->id]);
|
||||
|
||||
// Now lets tag these questions.
|
||||
\core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $context1, array('tag 1', 'tag 2'));
|
||||
\core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $context1, array('tag 3', 'tag 4'));
|
||||
\core_tag_tag::set_item_tags('core_question', 'question', $question3->id, $context2, array('tag 5', 'tag 6'));
|
||||
\core_tag_tag::set_item_tags('core_question', 'question', $question4->id, $context2, array('tag 7', 'tag 8'));
|
||||
\core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $context1, ['tag 1', 'tag 2']);
|
||||
\core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $context1, ['tag 3', 'tag 4']);
|
||||
\core_tag_tag::set_item_tags('core_question', 'question', $question3->id, $context2, ['tag 5', 'tag 6']);
|
||||
\core_tag_tag::set_item_tags('core_question', 'question', $question4->id, $context2, ['tag 7', 'tag 8']);
|
||||
|
||||
// Test moving the questions to another category.
|
||||
question_move_questions_to_category(array($question1->id, $question2->id), $questioncat2->id);
|
||||
|
||||
// Test that all tag_instances belong to one context.
|
||||
$this->assertEquals(8, $DB->count_records('tag_instance', array('component' => 'core_question',
|
||||
'contextid' => $questioncat2->contextid)));
|
||||
|
||||
// Test moving them back.
|
||||
question_move_questions_to_category(array($question1->id, $question2->id), $questioncat1->id);
|
||||
|
||||
// Test that all tag_instances are now reset to how they were initially.
|
||||
$this->assertEquals(4, $DB->count_records('tag_instance', array('component' => 'core_question',
|
||||
'contextid' => $questioncat1->contextid)));
|
||||
$this->assertEquals(4, $DB->count_records('tag_instance', array('component' => 'core_question',
|
||||
'contextid' => $questioncat2->contextid)));
|
||||
|
||||
// Now test moving a whole question category to another context.
|
||||
// Test moving a whole question category to another context.
|
||||
question_move_category_to_context($questioncat1->id, $questioncat1->contextid, $questioncat2->contextid);
|
||||
|
||||
// Test that all tag_instances belong to one context.
|
||||
$this->assertEquals(8, $DB->count_records('tag_instance', array('component' => 'core_question',
|
||||
'contextid' => $questioncat2->contextid)));
|
||||
$this->assertEquals(8, $DB->count_records('tag_instance', ['component' => 'core_question',
|
||||
'contextid' => $questioncat2->contextid]));
|
||||
|
||||
// Now test moving them back.
|
||||
question_move_category_to_context($questioncat1->id, $questioncat2->contextid,
|
||||
\context_coursecat::instance($coursecat1->id)->id);
|
||||
\context_module::instance($modqbank1->cmid)->id);
|
||||
|
||||
// Test that all tag_instances are now reset to how they were initially.
|
||||
$this->assertEquals(4, $DB->count_records('tag_instance', array('component' => 'core_question',
|
||||
'contextid' => $questioncat1->contextid)));
|
||||
$this->assertEquals(4, $DB->count_records('tag_instance', array('component' => 'core_question',
|
||||
'contextid' => $questioncat2->contextid)));
|
||||
|
||||
// Now we want to test deleting the course category and moving the questions to another category.
|
||||
question_delete_course_category($coursecat1, $coursecat2);
|
||||
|
||||
// Test that all tag_instances belong to one context.
|
||||
$this->assertEquals(8, $DB->count_records('tag_instance', array('component' => 'core_question',
|
||||
'contextid' => $questioncat2->contextid)));
|
||||
$this->assertEquals(4, $DB->count_records('tag_instance', ['component' => 'core_question',
|
||||
'contextid' => $questioncat1->contextid]));
|
||||
$this->assertEquals(4, $DB->count_records('tag_instance', ['component' => 'core_question',
|
||||
'contextid' => $questioncat2->contextid]));
|
||||
|
||||
// Create a course.
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$modqbank3 = $this->getDataGenerator()->create_module('qbank', ['course' => $course->id]);
|
||||
|
||||
// Create some question categories and questions in this course.
|
||||
$coursecontext = \context_course::instance($course->id);
|
||||
$questioncat = $questiongenerator->create_question_category(array('contextid' => $coursecontext->id));
|
||||
$question1 = $questiongenerator->create_question('shortanswer', null, array('category' => $questioncat->id));
|
||||
$question2 = $questiongenerator->create_question('shortanswer', null, array('category' => $questioncat->id));
|
||||
$modcontext = \context_module::instance($modqbank3->cmid);
|
||||
$questioncat = $questiongenerator->create_question_category(['contextid' => $modcontext->id]);
|
||||
$question1 = $questiongenerator->create_question('shortanswer', null, ['category' => $questioncat->id]);
|
||||
$question2 = $questiongenerator->create_question('shortanswer', null, ['category' => $questioncat->id]);
|
||||
|
||||
// Add some tags to these questions.
|
||||
\core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, array('tag 1', 'tag 2'));
|
||||
\core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, array('tag 1', 'tag 2'));
|
||||
\core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $modcontext, ['tag 1', 'tag 2']);
|
||||
\core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $modcontext, ['tag 1', 'tag 2']);
|
||||
|
||||
// Create a course that we are going to restore the other course to.
|
||||
$course2 = $this->getDataGenerator()->create_course();
|
||||
@ -260,9 +243,21 @@ class questionlib_test extends \advanced_testcase {
|
||||
$rc->execute_precheck();
|
||||
$rc->execute_plan();
|
||||
|
||||
$modinfo = get_fast_modinfo($course2);
|
||||
$qbanks = $modinfo->get_instances_of('qbank');
|
||||
$qbankids = array_column($qbanks, 'instance');
|
||||
$qbankrecords = $DB->get_records_list('qbank', 'id', $qbankids, '', 'id, type');
|
||||
$qbanks = array_filter($qbanks, static function($bank) use ($qbankrecords) {
|
||||
if (isset($qbankrecords[$bank->instance])) {
|
||||
return $qbankrecords[$bank->instance]->type === question_bank_helper::TYPE_STANDARD;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
$qbank = reset($qbanks);
|
||||
|
||||
// Get the created question category.
|
||||
$restoredcategory = $DB->get_record_select('question_categories', 'contextid = ? AND parent <> 0',
|
||||
array(\context_course::instance($course2->id)->id), '*', MUST_EXIST);
|
||||
[$qbank->context->id, '*', MUST_EXIST]);
|
||||
|
||||
// Check that there are two questions in the restored to course's context.
|
||||
$this->assertEquals(2, $DB->get_record_sql('SELECT COUNT(q.id) as questioncount
|
||||
|
@ -40,8 +40,9 @@ class backup_qbank_activity_task extends backup_activity_task {
|
||||
* Define (add) particular steps this activity can have
|
||||
*/
|
||||
protected function define_my_steps() {
|
||||
// Qbank only has one structure step
|
||||
$this->add_step(new backup_qbank_activity_structure_step('qbank_structure', 'qbank.xml'));
|
||||
$this->add_step(new backup_calculate_question_categories('qbank_activity_question_categories'));
|
||||
$this->add_step(new backup_delete_temp_questions('clean_temp_questions'));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -52,6 +53,13 @@ class backup_qbank_activity_task extends backup_activity_task {
|
||||
* @return string encoded content
|
||||
*/
|
||||
public static function encode_content_links($content) {
|
||||
return $content;
|
||||
global $CFG;
|
||||
|
||||
$base = preg_quote($CFG->wwwroot, '/');
|
||||
|
||||
// Link to qbank view by moduleid.
|
||||
$search = "/(".$base."\/mod\/qbank\/view.php\?id\=)([0-9]+)/";
|
||||
|
||||
return preg_replace($search, '$@QBANKVIEWBYID*$2@$', $content);
|
||||
}
|
||||
}
|
||||
|
@ -60,7 +60,11 @@ class restore_qbank_activity_task extends restore_activity_task {
|
||||
* @return restore_decode_rule[].
|
||||
*/
|
||||
public static function define_decode_rules(): array {
|
||||
return [];
|
||||
$rules = [];
|
||||
|
||||
$rules[] = new restore_decode_rule('QBANKVIEWBYID', '/mod/qbank/view.php?id=$1', 'course_module');
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -168,9 +168,13 @@ trait quizaccess_seb_test_helper_trait {
|
||||
$quiz->seb_showsebdownloadlink = 1;
|
||||
$quiz->coursemodule = $quiz->cmid;
|
||||
|
||||
// Create a question bank.
|
||||
$qbank = self::getDataGenerator()->create_module('qbank', ['course' => $course->id]);
|
||||
$qbankcontext = context_module::instance($qbank->cmid);
|
||||
|
||||
// Create a couple of questions.
|
||||
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
$cat = $questiongenerator->create_question_category();
|
||||
$cat = $questiongenerator->create_question_category(['contextid' => $qbankcontext->id]);
|
||||
|
||||
$saq = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
|
||||
quiz_add_quiz_question($saq->id, $quiz);
|
||||
|
@ -56,21 +56,21 @@ class quiz_question_restore_test extends \advanced_testcase {
|
||||
}
|
||||
|
||||
/**
|
||||
* Test a quiz backup and restore in a different course without attempts for course question bank.
|
||||
*
|
||||
* @covers \mod_quiz\question\bank\qbank_helper::get_question_structure
|
||||
*/
|
||||
public function test_quiz_restore_in_a_different_course_using_course_question_bank(): void {
|
||||
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.
|
||||
$coursecontext = \context_course::instance($this->course->id);
|
||||
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
$this->add_two_regular_questions($questiongenerator, $quiz, ['contextid' => $coursecontext->id]);
|
||||
$this->add_one_random_question($questiongenerator, $quiz, ['contextid' => $coursecontext->id]);
|
||||
$this->add_two_regular_questions($questiongenerator, $quiz, ['contextid' => $qbankcontext->id]);
|
||||
$this->add_one_random_question($questiongenerator, $quiz, ['contextid' => $qbankcontext->id]);
|
||||
|
||||
// Make the backup.
|
||||
$backupid = $this->backup_quiz($quiz, $this->user);
|
||||
@ -303,12 +303,24 @@ class quiz_question_restore_test extends \advanced_testcase {
|
||||
$rc = new \restore_controller($backupid, $newcourseid, \backup::INTERACTIVE_NO, \backup::MODE_GENERAL, $USER->id,
|
||||
\backup::TARGET_NEW_COURSE);
|
||||
|
||||
$this->assertTrue($rc->execute_precheck());
|
||||
$rc->execute_precheck();
|
||||
$results = $rc->get_precheck_results();
|
||||
// Backup contains categories attached to deprecated contexts so the results should only contain warnings for these.
|
||||
$this->assertCount(2, $results['warnings']);
|
||||
foreach ($results['warnings'] as $warning) {
|
||||
$this->assertStringContainsString('will be created at a question bank module context by restore', $warning);
|
||||
}
|
||||
$this->assertArrayNotHasKey('errors', $results);
|
||||
|
||||
$rc->execute_plan();
|
||||
$rc->destroy();
|
||||
|
||||
// Get the information about the resulting course and check that it is set up correctly.
|
||||
$modinfo = get_fast_modinfo($newcourseid);
|
||||
$qbanks = $modinfo->get_instances_of('qbank');
|
||||
$this->assertCount(1, $qbanks);
|
||||
$qbank = reset($qbanks);
|
||||
$this->assertEquals(get_string('systembank', 'question'), $qbank->name);
|
||||
$quiz = array_values($modinfo->get_instances_of('quiz'))[0];
|
||||
$quizobj = \mod_quiz\quiz_settings::create($quiz->instance);
|
||||
$structure = structure::create_for_quiz($quizobj);
|
||||
@ -322,10 +334,9 @@ class quiz_question_restore_test extends \advanced_testcase {
|
||||
$questions = $quizobj->get_questions();
|
||||
$this->assertCount(1, $questions);
|
||||
|
||||
// Count the questions for course question bank.
|
||||
$this->assertEquals(6, $this->question_count(\context_course::instance($newcourseid)->id));
|
||||
$this->assertEquals(6, $this->question_count(\context_course::instance($newcourseid)->id,
|
||||
"AND q.qtype <> 'random'"));
|
||||
// Count the questions for new course mod_qbank question bank.
|
||||
$this->assertEquals(6, $this->question_count(\context_module::instance($qbank->id)->id));
|
||||
$this->assertEquals(6, $this->question_count(\context_module::instance($qbank->id)->id, "AND q.qtype <> 'random'"));
|
||||
|
||||
// Count the questions in quiz qbank.
|
||||
$this->assertEquals(0, $this->question_count($quizobj->get_context()->id));
|
||||
@ -408,13 +419,20 @@ class quiz_question_restore_test extends \advanced_testcase {
|
||||
$rc = new \restore_controller($backupid, $newcourseid, \backup::INTERACTIVE_NO, \backup::MODE_GENERAL, $USER->id,
|
||||
\backup::TARGET_NEW_COURSE);
|
||||
|
||||
$this->assertTrue($rc->execute_precheck());
|
||||
$rc->execute_precheck();
|
||||
$results = $rc->get_precheck_results();
|
||||
// Backup contains categories attached to deprecated contexts, so we should only have warnings for those.
|
||||
$this->assertCount(1, $results['warnings']);
|
||||
$this->assertStringContainsString('will be created at a question bank module context by restore', $results['warnings'][0]);
|
||||
$this->assertArrayNotHasKey('errors', $results);
|
||||
|
||||
$rc->execute_plan();
|
||||
$rc->destroy();
|
||||
|
||||
// Get the information about the resulting course and check that it is set up correctly.
|
||||
// Each quiz should contain an instance of the random question.
|
||||
$modinfo = get_fast_modinfo($newcourseid);
|
||||
$qbank = array_values($modinfo->get_instances_of('qbank'))[0];
|
||||
$quizzes = $modinfo->get_instances_of('quiz');
|
||||
$this->assertCount(2, $quizzes);
|
||||
foreach ($quizzes as $quiz) {
|
||||
@ -431,10 +449,10 @@ class quiz_question_restore_test extends \advanced_testcase {
|
||||
$this->assertCount(1, $questions);
|
||||
}
|
||||
|
||||
// Count the questions for course question bank.
|
||||
// Count the questions for new course mod_qbank question bank.
|
||||
// We should have a single question, the random question should have been deleted after the restore.
|
||||
$this->assertEquals(1, $this->question_count(\context_course::instance($newcourseid)->id));
|
||||
$this->assertEquals(1, $this->question_count(\context_course::instance($newcourseid)->id,
|
||||
$this->assertEquals(1, $this->question_count(\context_module::instance($qbank->id)->id));
|
||||
$this->assertEquals(1, $this->question_count(\context_module::instance($qbank->id)->id,
|
||||
"AND q.qtype <> 'random'"));
|
||||
|
||||
// Count the questions in quiz qbank.
|
||||
|
@ -68,7 +68,15 @@ class restore_attempt_test extends \advanced_testcase {
|
||||
$controller = new restore_controller($backuptempdir, $courseid, backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
|
||||
backup::TARGET_NEW_COURSE);
|
||||
|
||||
$this->assertTrue($controller->execute_precheck());
|
||||
$controller->execute_precheck();
|
||||
$results = $controller->get_precheck_results();
|
||||
// Backup contains categories attached to deprecated contexts so the results should only contain warnings for these.
|
||||
$this->assertCount(2, $results['warnings']);
|
||||
foreach ($results['warnings'] as $warning) {
|
||||
$this->assertStringContainsString('will be created at a question bank module context by restore', $warning);
|
||||
}
|
||||
$this->assertArrayNotHasKey('errors', $results);
|
||||
|
||||
$controller->execute_plan();
|
||||
$controller->destroy();
|
||||
|
||||
|
@ -49,7 +49,14 @@ class tags_test extends \advanced_testcase {
|
||||
$rc = new \restore_controller($backupid, $newcourseid, \backup::INTERACTIVE_NO, \backup::MODE_GENERAL, $USER->id,
|
||||
\backup::TARGET_NEW_COURSE);
|
||||
|
||||
$this->assertTrue($rc->execute_precheck());
|
||||
$rc->execute_precheck();
|
||||
$results = $rc->get_precheck_results();
|
||||
// Backup contains categories attached to deprecated contexts so the results should only contain warnings for these.
|
||||
$this->assertCount(2, $results['warnings']);
|
||||
foreach ($results['warnings'] as $warning) {
|
||||
$this->assertStringContainsString('will be created at a question bank module context by restore', $warning);
|
||||
}
|
||||
$this->assertArrayNotHasKey('errors', $results);
|
||||
$rc->execute_plan();
|
||||
$rc->destroy();
|
||||
|
||||
@ -85,7 +92,13 @@ class tags_test extends \advanced_testcase {
|
||||
$slottags = explode(',', $slottags);
|
||||
$this->assertEquals("{$tag2->id},{$tag2->name}", "{$slottags[0]},{$slottags[1]}");
|
||||
|
||||
$defaultcategory = question_get_default_category(\context_course::instance($newcourseid)->id);
|
||||
// Course context question cats get restored to a default qbank module instance.
|
||||
$modinfo = get_fast_modinfo($newcourseid);
|
||||
$qbanks = $modinfo->get_instances_of('qbank');
|
||||
$this->assertCount(1, $qbanks);
|
||||
$qbank = reset($qbanks);
|
||||
|
||||
$defaultcategory = question_get_default_category(\context_module::instance($qbank->id)->id, true);
|
||||
$this->assertEquals($defaultcategory->id, $question->category);
|
||||
$randomincludingsubcategories = $DB->get_record('question_set_references',
|
||||
['itemid' => reset($slots)->id, 'component' => 'mod_quiz', 'questionarea' => 'slot']);
|
||||
|
@ -43,32 +43,27 @@ class helper_test extends \advanced_testcase {
|
||||
|
||||
// Create a course and an activity.
|
||||
$course = $generator->create_course();
|
||||
$qbank = self::getDataGenerator()->create_module('qbank', ['course' => $course->id]);
|
||||
$qbankcontext = \context_module::instance($qbank->cmid);
|
||||
$quiz = $generator->create_module('quiz', ['course' => $course->id]);
|
||||
|
||||
// Create a question in each place.
|
||||
$questiongenerator = $generator->get_plugin_generator('core_question');
|
||||
$courseqcat = $questiongenerator->create_question_category(['contextid' => context_course::instance($course->id)->id]);
|
||||
$courseq = $questiongenerator->create_question('truefalse', null, ['category' => $courseqcat->id]);
|
||||
$qbankqcat = $questiongenerator->create_question_category(['contextid' => $qbankcontext->id]);
|
||||
$qbankq = $questiongenerator->create_question('truefalse', null, ['category' => $qbankqcat->id]);
|
||||
$quizqcat = $questiongenerator->create_question_category(['contextid' => context_module::instance($quiz->cmid)->id]);
|
||||
$quizq = $questiongenerator->create_question('truefalse', null, ['category' => $quizqcat->id]);
|
||||
$systemqcat = $questiongenerator->create_question_category();
|
||||
$systemq = $questiongenerator->create_question('truefalse', null, ['category' => $systemqcat->id]);
|
||||
|
||||
// Verify some URLs.
|
||||
$this->assertEquals(new moodle_url('/question/bank/exporttoxml/exportone.php',
|
||||
['id' => $courseq->id, 'courseid' => $course->id, 'sesskey' => sesskey()]),
|
||||
['id' => $qbankq->id, 'cmid' => $qbank->cmid, 'sesskey' => sesskey()]),
|
||||
helper::question_get_export_single_question_url(
|
||||
question_bank::load_question_data($courseq->id)));
|
||||
question_bank::load_question_data($qbankq->id)));
|
||||
|
||||
$this->assertEquals(new moodle_url('/question/bank/exporttoxml/exportone.php',
|
||||
['id' => $quizq->id, 'cmid' => $quiz->cmid, 'sesskey' => sesskey()]),
|
||||
helper::question_get_export_single_question_url(
|
||||
question_bank::load_question($quizq->id)));
|
||||
|
||||
$this->assertEquals(new moodle_url('/question/bank/exporttoxml/exportone.php',
|
||||
['id' => $systemq->id, 'courseid' => SITEID, 'sesskey' => sesskey()]),
|
||||
helper::question_get_export_single_question_url(
|
||||
question_bank::load_question($systemq->id)));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -22,6 +22,11 @@ use question_bank;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use backup;
|
||||
use core_question\local\bank\question_bank_helper;
|
||||
use restore_controller;
|
||||
use restore_dbops;
|
||||
|
||||
global $CFG;
|
||||
require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
|
||||
require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
|
||||
@ -63,24 +68,43 @@ class backup_test extends \advanced_testcase {
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores a backup that has been made earlier.
|
||||
* Makes a backup of a course module.
|
||||
*
|
||||
* @param string $backupid The unique identifier of the backup.
|
||||
* @param string $fullname Full name of the new course that is going to be created.
|
||||
* @param string $shortname Short name of the new course that is going to be created.
|
||||
* @param int $categoryid The course category the backup is going to be restored in.
|
||||
* @param string[] $expectedprecheckwarning
|
||||
* @return int The new course id.
|
||||
* @param int $modid The course_module id.
|
||||
* @return string Unique identifier for this backup.
|
||||
*/
|
||||
protected function restore_course($backupid, $fullname, $shortname, $categoryid, $expectedprecheckwarning = []) {
|
||||
protected function backup_course_module(int $modid) {
|
||||
global $CFG, $USER;
|
||||
|
||||
// Turn off file logging, otherwise it can't delete the file (Windows).
|
||||
$CFG->backup_file_logger_level = \backup::LOG_NONE;
|
||||
|
||||
// Do restore to new course with default settings.
|
||||
$newcourseid = \restore_dbops::create_new_course($fullname, $shortname, $categoryid);
|
||||
$rc = new \restore_controller($backupid, $newcourseid,
|
||||
// Do backup with default settings. MODE_IMPORT means it will just
|
||||
// create the directory and not zip it.
|
||||
$bc = new \backup_controller(\backup::TYPE_1ACTIVITY, $modid,
|
||||
\backup::FORMAT_MOODLE, \backup::INTERACTIVE_NO, \backup::MODE_IMPORT,
|
||||
$USER->id);
|
||||
$backupid = $bc->get_backupid();
|
||||
$bc->execute_plan();
|
||||
$bc->destroy();
|
||||
|
||||
return $backupid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores a backup that has been made earlier.
|
||||
*
|
||||
* @param string $backupid The unique identifier of the backup.
|
||||
* @param int $courseid Course id of where the restore is happening.
|
||||
* @param string[] $expectedprecheckwarning
|
||||
*/
|
||||
protected function restore_to_course(string $backupid, int $courseid, array $expectedprecheckwarning = []): void {
|
||||
global $CFG, $USER;
|
||||
|
||||
// Turn off file logging, otherwise it can't delete the file (Windows).
|
||||
$CFG->backup_file_logger_level = \backup::LOG_NONE;
|
||||
|
||||
$rc = new \restore_controller($backupid, $courseid,
|
||||
\backup::INTERACTIVE_NO, \backup::MODE_GENERAL, $USER->id,
|
||||
\backup::TARGET_NEW_COURSE);
|
||||
|
||||
@ -94,12 +118,10 @@ class backup_test extends \advanced_testcase {
|
||||
}
|
||||
$rc->execute_plan();
|
||||
$rc->destroy();
|
||||
|
||||
return $newcourseid;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function tests backup and restore of question tags and course level question tags.
|
||||
* This function tests backup and restore of question tags.
|
||||
*/
|
||||
public function test_backup_question_tags(): void {
|
||||
global $DB;
|
||||
@ -107,7 +129,7 @@ class backup_test extends \advanced_testcase {
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
// Create a new course category and and a new course in that.
|
||||
// Create a new course category and a new course in that.
|
||||
$category1 = $this->getDataGenerator()->create_category();
|
||||
$course = $this->getDataGenerator()->create_course(['category' => $category1->id]);
|
||||
$courseshortname = $course->shortname;
|
||||
@ -115,18 +137,17 @@ class backup_test extends \advanced_testcase {
|
||||
|
||||
// Create 2 questions.
|
||||
$qgen = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
$context = \context_coursecat::instance($category1->id);
|
||||
$qbank = $this->getDataGenerator()->create_module('qbank', ['course' => $course->id]);
|
||||
$context = \context_module::instance($qbank->cmid);
|
||||
$qcat = $qgen->create_question_category(['contextid' => $context->id]);
|
||||
$question1 = $qgen->create_question('shortanswer', null, ['category' => $qcat->id, 'idnumber' => 'q1']);
|
||||
$question2 = $qgen->create_question('shortanswer', null, ['category' => $qcat->id, 'idnumber' => 'q2']);
|
||||
|
||||
// Tag the questions with 2 question tags and 2 course level question tags.
|
||||
// Tag the questions with 2 question tags.
|
||||
$qcontext = \context::instance_by_id($qcat->contextid);
|
||||
$coursecontext = context_course::instance($course->id);
|
||||
\core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $qcontext, ['qtag1', 'qtag2']);
|
||||
\core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $qcontext, ['qtag3', 'qtag4']);
|
||||
\core_tag_tag::set_item_tags('core_question', 'question', $question1->id, $coursecontext, ['ctag1', 'ctag2']);
|
||||
\core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['ctag3', 'ctag4']);
|
||||
|
||||
// Create a quiz and add one of the questions to that.
|
||||
$quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id]);
|
||||
@ -142,10 +163,20 @@ class backup_test extends \advanced_testcase {
|
||||
question_delete_question($question2->id);
|
||||
|
||||
// Restore the backup we had made earlier into a new course.
|
||||
$courseid2 = $this->restore_course($backupid1, $coursefullname, $courseshortname . '_2', $category1->id);
|
||||
// Do restore to new course with default settings.
|
||||
$courseid2 = \restore_dbops::create_new_course($coursefullname, $courseshortname . '_2', $category1->id);
|
||||
$this->restore_to_course($backupid1, $courseid2);
|
||||
$modinfo = get_fast_modinfo($courseid2);
|
||||
$qbanks = $modinfo->get_instances_of('qbank');
|
||||
$qbanks = array_filter($qbanks, static fn($qbank) => $qbank->get_name() === 'Question bank 1');
|
||||
$this->assertCount(1, $qbanks);
|
||||
$qbank = reset($qbanks);
|
||||
$qbankcontext = \context_module::instance($qbank->id);
|
||||
$cats = $DB->get_records_select('question_categories' , 'parent <> 0', ['contextid' => $qbankcontext->id]);
|
||||
$this->assertCount(1, $cats);
|
||||
$cat = reset($cats);
|
||||
|
||||
// The questions should remain in the question category they were which is
|
||||
// a question category belonging to a course category context.
|
||||
// The questions should be restored to a mod_qbank context in the new course.
|
||||
$sql = 'SELECT q.*,
|
||||
qbe.idnumber
|
||||
FROM {question} q
|
||||
@ -153,7 +184,7 @@ class backup_test extends \advanced_testcase {
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
WHERE qbe.questioncategoryid = ?
|
||||
ORDER BY qbe.idnumber';
|
||||
$questions = $DB->get_records_sql($sql, [$qcat->id]);
|
||||
$questions = $DB->get_records_sql($sql, [$cat->id]);
|
||||
$this->assertCount(2, $questions);
|
||||
|
||||
// Retrieve tags for each question and check if they are assigned at the right context.
|
||||
@ -162,15 +193,10 @@ class backup_test extends \advanced_testcase {
|
||||
$tags = \core_tag_tag::get_item_tags('core_question', 'question', $question->id);
|
||||
|
||||
// Each question is tagged with 4 tags (2 question tags + 2 course tags).
|
||||
$this->assertCount(4, $tags);
|
||||
$this->assertCount(2, $tags);
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
if (in_array($tag->name, ['ctag1', 'ctag2', 'ctag3', 'ctag4'])) {
|
||||
$expected = context_course::instance($courseid2)->id;
|
||||
} else if (in_array($tag->name, ['qtag1', 'qtag2', 'qtag3', 'qtag4'])) {
|
||||
$expected = $qcontext->id;
|
||||
}
|
||||
$this->assertEquals($expected, $tag->taginstancecontextid);
|
||||
$this->assertEquals($qbankcontext->id, $tag->taginstancecontextid);
|
||||
}
|
||||
|
||||
// Also check idnumbers have been backed up and restored.
|
||||
@ -188,14 +214,15 @@ class backup_test extends \advanced_testcase {
|
||||
// Create a new course category to restore the backup file into it.
|
||||
$category2 = $this->getDataGenerator()->create_category();
|
||||
|
||||
$expectedwarnings = [
|
||||
get_string('qcategory2coursefallback', 'backup', (object) ['name' => 'top']),
|
||||
get_string('qcategory2coursefallback', 'backup', (object) ['name' => $qcat->name])
|
||||
];
|
||||
|
||||
// Restore to a new course in the new course category.
|
||||
$courseid3 = $this->restore_course($backupid2, $coursefullname, $courseshortname . '_3', $category2->id, $expectedwarnings);
|
||||
$coursecontext3 = context_course::instance($courseid3);
|
||||
$courseid3 = \restore_dbops::create_new_course($coursefullname, $courseshortname . '_3', $category2->id);
|
||||
$this->restore_to_course($backupid2, $courseid3);
|
||||
$modinfo = get_fast_modinfo($courseid3);
|
||||
$qbanks = $modinfo->get_instances_of('qbank');
|
||||
$qbanks = array_filter($qbanks, static fn($qbank) => $qbank->get_name() === 'Question bank 1');
|
||||
$this->assertCount(1, $qbanks);
|
||||
$qbank = reset($qbanks);
|
||||
$context = \context_module::instance($qbank->id);
|
||||
|
||||
// The questions should have been moved to a question category that belongs to a course context.
|
||||
$questions = $DB->get_records_sql("SELECT q.*
|
||||
@ -203,18 +230,18 @@ class backup_test extends \advanced_testcase {
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
|
||||
WHERE qc.contextid = ?", [$coursecontext3->id]);
|
||||
WHERE qc.contextid = ?", [$context->id]);
|
||||
$this->assertCount(2, $questions);
|
||||
|
||||
// Now, retrieve tags for each question and check if they are assigned at the right context.
|
||||
foreach ($questions as $question) {
|
||||
$tags = \core_tag_tag::get_item_tags('core_question', 'question', $question->id);
|
||||
|
||||
// Each question is tagged with 4 tags (all are course tags now).
|
||||
$this->assertCount(4, $tags);
|
||||
// Each question is tagged with 2 tags (all are question context tags now).
|
||||
$this->assertCount(2, $tags);
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
$this->assertEquals($coursecontext3->id, $tag->taginstancecontextid);
|
||||
$this->assertEquals($context->id, $tag->taginstancecontextid);
|
||||
}
|
||||
}
|
||||
|
||||
@ -462,4 +489,262 @@ class backup_test extends \advanced_testcase {
|
||||
$this->assertEquals('The reason: ' . $recodedcontent, $restoredquestion->options->answers[$firstanswerid]->feedback);
|
||||
$this->assertEquals('Hint: ' . $recodedcontent, $restoredquestion->hints[$firsthintid]->hint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Boilerplate setup for the tests. Creates a course, a quiz, and a qbank module. It adds a category to each module context
|
||||
* and adds a question to each category. Finally, it adds the 2 questions to the quiz.
|
||||
*
|
||||
* @return \stdClass
|
||||
*/
|
||||
private function add_course_quiz_and_qbank() {
|
||||
$qgen = self::getDataGenerator()->get_plugin_generator('core_question');
|
||||
|
||||
// Create a new course.
|
||||
$course = self::getDataGenerator()->create_course();
|
||||
|
||||
// Create a question bank module instance, a category for that module, and a question for that category.
|
||||
$qbank = self::getDataGenerator()->create_module(
|
||||
'qbank',
|
||||
['type' => question_bank_helper::TYPE_STANDARD, 'course' => $course->id]
|
||||
);
|
||||
$qbankcontext = \context_module::instance($qbank->cmid);
|
||||
$bankqcat = $qgen->create_question_category(['contextid' => $qbankcontext->id]);
|
||||
$bankquestion = $qgen->create_question('shortanswer',
|
||||
null,
|
||||
['name' => 'bank question', 'category' => $bankqcat->id, 'idnumber' => 'bankq1']
|
||||
);
|
||||
|
||||
// Create a quiz module instance, a category for that module, and a question for that category.
|
||||
$quiz = self::getDataGenerator()->create_module('quiz', ['course' => $course->id]);
|
||||
$quizcontext = \context_module::instance($quiz->cmid);
|
||||
$quizqcat = $qgen->create_question_category(['contextid' => $quizcontext->id]);
|
||||
$quizquestion = $qgen->create_question('shortanswer',
|
||||
null,
|
||||
['name' => 'quiz question', 'category' => $quizqcat->id, 'idnumber' => 'quizq1']
|
||||
);
|
||||
|
||||
quiz_add_quiz_question($bankquestion->id, $quiz);
|
||||
quiz_add_quiz_question($quizquestion->id, $quiz);
|
||||
|
||||
$data = new \stdClass();
|
||||
$data->course = $course;
|
||||
$data->qbank = $qbank;
|
||||
$data->qbankcategory = $bankqcat;
|
||||
$data->qbankquestion = $bankquestion;
|
||||
$data->quiz = $quiz;
|
||||
$data->quizcategory = $quizqcat;
|
||||
$data->quizquestion = $quizquestion;
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the backup contains ONLY a quiz but that quiz uses questions from a qbank module and itself,
|
||||
* 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;
|
||||
|
||||
$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();
|
||||
delete_course($data->course->id, false);
|
||||
|
||||
$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 'system' type default 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 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.
|
||||
*
|
||||
* @return void
|
||||
* @covers \restore_controller::execute_plan()
|
||||
*/
|
||||
public function test_bank_and_quiz_activity_restore_to_new_course(): void {
|
||||
// Create a new course.
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
self::setAdminUser();
|
||||
|
||||
// Create a course to make a backup from.
|
||||
$data = $this->add_course_quiz_and_qbank();
|
||||
$oldcourse = $data->course;
|
||||
|
||||
// Backup the course.
|
||||
$backupid = $this->backup_course($oldcourse);
|
||||
|
||||
// Create a new course to restore to.
|
||||
$newcourse = self::getDataGenerator()->create_course();
|
||||
|
||||
// Restore it.
|
||||
$this->restore_to_course($backupid, $newcourse->id);
|
||||
|
||||
// Assert the quiz got its question catregories restored.
|
||||
$modinfo = get_fast_modinfo($newcourse);
|
||||
$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]
|
||||
);
|
||||
$quizcat = reset($quizcats);
|
||||
$quizcatqs = get_questions_category($quizcat, false);
|
||||
$this->assertCount(1, $quizcatqs);
|
||||
$quizcatq = reset($quizcatqs);
|
||||
$this->assertEquals('quiz question', $quizcatq->name);
|
||||
|
||||
// Assert the qbank got its questions restored to the module in the backup.
|
||||
$qbanks = $modinfo->get_instances_of('qbank');
|
||||
$qbanks = array_filter($qbanks, static function($bank) {
|
||||
global $DB;
|
||||
$modrecord = $DB->get_record('qbank', ['id' => $bank->instance]);
|
||||
return $modrecord->type === question_bank_helper::TYPE_STANDARD;
|
||||
});
|
||||
$this->assertCount(1, $qbanks);
|
||||
$qbank = reset($qbanks);
|
||||
$bankcats = $DB->get_records_select('question_categories',
|
||||
'parent <> 0 AND contextid = :contextid',
|
||||
['contextid' => \context_module::instance($qbank->id)->id]
|
||||
);
|
||||
$bankcat = reset($bankcats);
|
||||
$bankqs = get_questions_category($bankcat, false);
|
||||
$this->assertCount(1, $bankqs);
|
||||
$bankq = reset($bankqs);
|
||||
$this->assertEquals('bank question', $bankq->name);
|
||||
}
|
||||
|
||||
/**
|
||||
* The course backup file contains question banks and a quiz module.
|
||||
* There is 1 question bank category per deprecated context level i.e. CONTEXT_SYSTEM, CONTEXT_COURSECAT, and CONTEXT_COURSE.
|
||||
* The quiz included in the backup uses a question in each category.
|
||||
*
|
||||
* @return void
|
||||
* @covers \restore_controller::execute_plan()
|
||||
*/
|
||||
public function test_pre_46_course_restore_to_new_course(): void {
|
||||
global $DB, $USER;
|
||||
self::setAdminUser();
|
||||
$this->resetAfterTest();
|
||||
|
||||
$backupid = 'question_category_45_format';
|
||||
$backuppath = make_backup_temp_directory($backupid);
|
||||
get_file_packer('application/vnd.moodle.backup')->extract_to_pathname(
|
||||
__DIR__ . "/fixtures/{$backupid}.mbz",
|
||||
$backuppath
|
||||
);
|
||||
|
||||
// Do restore to new course with default settings.
|
||||
$categoryid = $DB->get_field_sql("SELECT MIN(id) FROM {course_categories}");
|
||||
$newcourseid = restore_dbops::create_new_course('Test fullname', 'Test shortname', $categoryid);
|
||||
$rc = new restore_controller($backupid, $newcourseid,
|
||||
backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
|
||||
backup::TARGET_NEW_COURSE
|
||||
);
|
||||
|
||||
$rc->execute_precheck();
|
||||
$rc->execute_plan();
|
||||
$rc->destroy();
|
||||
|
||||
$modinfo = get_fast_modinfo($newcourseid);
|
||||
|
||||
$qbanks = $modinfo->get_instances_of('qbank');
|
||||
$qbanks = array_filter($qbanks, static function($bank) {
|
||||
global $DB;
|
||||
$modrecord = $DB->get_record('qbank', ['id' => $bank->instance]);
|
||||
return $modrecord->type === question_bank_helper::TYPE_SYSTEM;
|
||||
});
|
||||
$this->assertCount(1, $qbanks);
|
||||
$qbank = reset($qbanks);
|
||||
$qbankcontext = \context_module::instance($qbank->id);
|
||||
$bankcats = $DB->get_records_select('question_categories',
|
||||
'parent <> 0 AND contextid = :contextid',
|
||||
['contextid' => $qbankcontext->id],
|
||||
'name ASC'
|
||||
);
|
||||
// The categories and questions in the 3 deprecated contexts
|
||||
// all got moved to the new default qbank module instance on the new course.
|
||||
$this->assertCount(3, $bankcats);
|
||||
$expectedidentifiers = [
|
||||
'Default for Category 1',
|
||||
'Default for System',
|
||||
'Default for Test Course 1',
|
||||
'Default for Quiz',
|
||||
];
|
||||
$i = 0;
|
||||
|
||||
foreach ($bankcats as $bankcat) {
|
||||
$identifer = $expectedidentifiers[$i];
|
||||
$this->assertEquals($identifer, $bankcat->name);
|
||||
$bankcatqs = get_questions_category($bankcat, false);
|
||||
$this->assertCount(1, $bankcatqs);
|
||||
$bankcatq = reset($bankcatqs);
|
||||
$this->assertEquals($identifer, $bankcatq->name);
|
||||
$i++;
|
||||
}
|
||||
|
||||
// The question category and question attached to the quiz got restored to its own context correctly.
|
||||
$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]
|
||||
);
|
||||
$quizcat = reset($quizcats);
|
||||
$quizcatqs = get_questions_category($quizcat, false);
|
||||
$this->assertCount(1, $quizcatqs);
|
||||
$quizcatq = reset($quizcatqs);
|
||||
$this->assertEquals($expectedidentifiers[$i], $quizcatq->name);
|
||||
}
|
||||
}
|
||||
|
BIN
question/tests/fixtures/question_category_45_format.mbz
vendored
Normal file
BIN
question/tests/fixtures/question_category_45_format.mbz
vendored
Normal file
Binary file not shown.
@ -71,7 +71,8 @@ class core_question_generator extends component_generator_base {
|
||||
if (isset($record['parent'])) {
|
||||
$record['contextid'] = $DB->get_field('question_categories', 'contextid', ['id' => $record['parent']]);
|
||||
} else {
|
||||
$record['contextid'] = context_system::instance()->id;
|
||||
$qbank = $this->datagenerator->create_module('qbank', ['course' => SITEID]);
|
||||
$record['contextid'] = context_module::instance($qbank->cmid)->id;
|
||||
}
|
||||
}
|
||||
if (!isset($record['parent'])) {
|
||||
@ -162,33 +163,20 @@ class core_question_generator extends component_generator_base {
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup a course category, course, a question category, and 2 questions
|
||||
* for testing.
|
||||
* Set up a course category, a course, a mod_qbank instance, a question category for the mod_qbank instance,
|
||||
* and 2 questions for testing.
|
||||
*
|
||||
* @param string $type The type of question category to create.
|
||||
* @return array The created data objects
|
||||
* @return array of the data objects mentioned above
|
||||
*/
|
||||
public function setup_course_and_questions($type = 'course') {
|
||||
public function setup_course_and_questions() {
|
||||
$datagenerator = $this->datagenerator;
|
||||
$category = $datagenerator->create_category();
|
||||
$course = $datagenerator->create_course([
|
||||
'numsections' => 5,
|
||||
'category' => $category->id
|
||||
]);
|
||||
|
||||
switch ($type) {
|
||||
case 'category':
|
||||
$context = context_coursecat::instance($category->id);
|
||||
break;
|
||||
|
||||
case 'system':
|
||||
$context = context_system::instance();
|
||||
break;
|
||||
|
||||
default:
|
||||
$context = context_course::instance($course->id);
|
||||
break;
|
||||
}
|
||||
$qbank = $datagenerator->create_module('qbank', ['course' => $course->id]);
|
||||
$context = context_module::instance($qbank->cmid);
|
||||
|
||||
$qcat = $this->create_question_category(['contextid' => $context->id]);
|
||||
|
||||
@ -197,7 +185,7 @@ class core_question_generator extends component_generator_base {
|
||||
$this->create_question('shortanswer', null, ['category' => $qcat->id]),
|
||||
];
|
||||
|
||||
return [$category, $course, $qcat, $questions];
|
||||
return [$category, $course, $qcat, $questions, $qbank];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -197,15 +197,15 @@ class provider_test extends \core_privacy\tests\provider_testcase {
|
||||
// Create one question as each user in diferent contexts.
|
||||
$this->setUser($user);
|
||||
$userdata = $questiongenerator->setup_course_and_questions();
|
||||
$expectedcontext = \context_course::instance($userdata[1]->id);
|
||||
$expectedcontext = \context_module::instance($userdata[4]->cmid);
|
||||
|
||||
$this->setUser($otheruser);
|
||||
$otheruserdata = $questiongenerator->setup_course_and_questions();
|
||||
$unexpectedcontext = \context_course::instance($otheruserdata[1]->id);
|
||||
$unexpectedcontext = \context_module::instance($otheruserdata[4]->cmid);
|
||||
|
||||
// And create another one where we'll update a question as the test user.
|
||||
$moreotheruserdata = $questiongenerator->setup_course_and_questions();
|
||||
$otherexpectedcontext = \context_course::instance($moreotheruserdata[1]->id);
|
||||
$otherexpectedcontext = \context_module::instance($moreotheruserdata[4]->cmid);
|
||||
$morequestions = $moreotheruserdata[3];
|
||||
|
||||
// Update the third set of questions.
|
||||
@ -451,17 +451,17 @@ class provider_test extends \core_privacy\tests\provider_testcase {
|
||||
$this->setUser($user2);
|
||||
$user2data = $questiongenerator->setup_course_and_questions();
|
||||
|
||||
$course1context = \context_course::instance($user1data[1]->id);
|
||||
$course1questions = $user1data[3];
|
||||
$qbankcontext = \context_module::instance($user1data[4]->cmid);
|
||||
$questions = $user1data[3];
|
||||
|
||||
// Log in as user3 and update the questions in course1.
|
||||
$this->setUser($user3);
|
||||
|
||||
foreach ($course1questions as $question) {
|
||||
foreach ($questions as $question) {
|
||||
$questiongenerator->update_question($question);
|
||||
}
|
||||
|
||||
$userlist = new \core_privacy\local\request\userlist($course1context, 'core_question');
|
||||
$userlist = new \core_privacy\local\request\userlist($qbankcontext, 'core_question');
|
||||
provider::get_users_in_context($userlist);
|
||||
|
||||
// User1 has created questions and user3 has edited them.
|
||||
@ -486,16 +486,16 @@ class provider_test extends \core_privacy\tests\provider_testcase {
|
||||
|
||||
// Create one question as each user in different contexts.
|
||||
$this->setUser($user1);
|
||||
$course1data = $questiongenerator->setup_course_and_questions();
|
||||
$course1 = $course1data[1];
|
||||
$course1qcat = $course1data[2];
|
||||
$course1questions = $course1data[3];
|
||||
$course1context = \context_course::instance($course1->id);
|
||||
$coursedata = $questiongenerator->setup_course_and_questions();
|
||||
$qbank = $coursedata[4];
|
||||
$course1qcat = $coursedata[2];
|
||||
$questions = $coursedata[3];
|
||||
$qbankcontext = \context_module::instance($qbank->cmid);
|
||||
|
||||
// Log in as user2 and update the questions in course1.
|
||||
$this->setUser($user2);
|
||||
|
||||
foreach ($course1questions as $question) {
|
||||
foreach ($questions as $question) {
|
||||
$questiongenerator->update_question($question);
|
||||
}
|
||||
|
||||
@ -508,7 +508,7 @@ class provider_test extends \core_privacy\tests\provider_testcase {
|
||||
$this->setUser($user1);
|
||||
$questiongenerator->setup_course_and_questions();
|
||||
|
||||
$approveduserlist = new \core_privacy\local\request\approved_userlist($course1context, 'core_question',
|
||||
$approveduserlist = new \core_privacy\local\request\approved_userlist($qbankcontext, 'core_question',
|
||||
[$user1->id, $user2->id]);
|
||||
provider::delete_data_for_users($approveduserlist);
|
||||
|
||||
@ -521,7 +521,7 @@ class provider_test extends \core_privacy\tests\provider_testcase {
|
||||
JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
|
||||
WHERE qc.contextid = ?
|
||||
AND (q.createdby = ? OR q.modifiedby = ? OR q.createdby = ? OR q.modifiedby = ?)",
|
||||
[$course1context->id, $user1->id, $user1->id, $user2->id, $user2->id])
|
||||
[$qbankcontext->id, $user1->id, $user1->id, $user2->id, $user2->id])
|
||||
);
|
||||
|
||||
// User3 data in course1 should not change.
|
||||
@ -532,7 +532,7 @@ class provider_test extends \core_privacy\tests\provider_testcase {
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
|
||||
WHERE qc.contextid = ? AND (q.createdby = ? OR q.modifiedby = ?)",
|
||||
[$course1context->id, $user3->id, $user3->id])
|
||||
[$qbankcontext->id, $user3->id, $user3->id])
|
||||
);
|
||||
|
||||
// User1 has authored 2 questions in another course.
|
||||
|
@ -43,8 +43,9 @@ class restore_test extends \restore_date_testcase {
|
||||
// Create a course with one essay question in its question bank.
|
||||
$generator = $this->getDataGenerator();
|
||||
$course = $generator->create_course();
|
||||
$contexts = new \core_question\local\bank\question_edit_contexts(\context_course::instance($course->id));
|
||||
$category = question_make_default_categories($contexts->all());
|
||||
$qbank = $generator->create_module('qbank', ['course' => $course->id]);
|
||||
$context = \context_module::instance($qbank->cmid);
|
||||
$category = question_make_default_categories([$context]);
|
||||
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
$essay = $questiongenerator->create_question('essay', null, array('category' => $category->id));
|
||||
|
||||
@ -54,9 +55,15 @@ class restore_test extends \restore_date_testcase {
|
||||
// Do backup and restore.
|
||||
$newcourseid = $this->backup_and_restore($course);
|
||||
|
||||
$modinfo = get_fast_modinfo($newcourseid);
|
||||
$newqbanks = array_filter(
|
||||
$modinfo->get_instances_of('qbank'),
|
||||
static fn($qbank) => $qbank->get_name() === 'Question bank 1'
|
||||
);
|
||||
$newqbank = reset($newqbanks);
|
||||
|
||||
// Verify that the restored question has options.
|
||||
$contexts = new \core_question\local\bank\question_edit_contexts(\context_course::instance($newcourseid));
|
||||
$newcategory = question_make_default_categories($contexts->all());
|
||||
$newcategory = question_make_default_categories([\context_module::instance($newqbank->id)]);
|
||||
$newessay = $DB->get_record_sql('SELECT q.*
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
|
@ -100,7 +100,9 @@ class events_test extends \advanced_testcase {
|
||||
global $DB;
|
||||
|
||||
// Create a course to tag.
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$course = self::getDataGenerator()->create_course();
|
||||
$qbank = self::getDataGenerator()->create_module('qbank', ['course' => $course->id]);
|
||||
$qbankcontext = \context_module::instance($qbank->cmid);
|
||||
|
||||
// Trigger and capture the event for tagging a course.
|
||||
$sink = $this->redirectEvents();
|
||||
@ -115,7 +117,7 @@ class events_test extends \advanced_testcase {
|
||||
|
||||
// Create a question to tag.
|
||||
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
$cat = $questiongenerator->create_question_category();
|
||||
$cat = $questiongenerator->create_question_category(['contextid' => $qbankcontext->id]);
|
||||
$question = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id));
|
||||
|
||||
// Trigger and capture the event for tagging a question.
|
||||
@ -129,7 +131,7 @@ class events_test extends \advanced_testcase {
|
||||
// Check that the tag was added to the question and the event data is valid.
|
||||
$this->assertEquals(1, $DB->count_records('tag_instance', array('component' => 'core')));
|
||||
$this->assertInstanceOf('\core\event\tag_added', $event);
|
||||
$this->assertEquals(\context_system::instance(), $event->get_context());
|
||||
$this->assertEquals($qbankcontext, $event->get_context());
|
||||
}
|
||||
|
||||
/**
|
||||
|
Loading…
x
Reference in New Issue
Block a user