Merge branch 'MDL-61132-master' of git://github.com/rezaies/moodle

This commit is contained in:
Andrew Nicols
2018-02-06 14:33:41 +08:00
25 changed files with 371 additions and 78 deletions
+42 -6
View File
@@ -4394,6 +4394,21 @@ class restore_create_categories_and_questions extends restore_structure_step {
}
$data->contextid = $mapping->parentitemid;
// 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.
$backuprelease = floatval($this->get_task()->get_info()->backup_release);
preg_match('/(\d{8})/', $this->get_task()->get_info()->moodle_release, $matches);
$backupbuild = (int)$matches[1];
$before35 = false;
if ($backuprelease < 3.5 || $backupbuild < 20180205) {
$before35 = true;
}
if (empty($mapping->info->parent) &&
($before35 || $mapping->info->contextlevel == CONTEXT_MODULE)) {
$top = question_get_top_category($data->contextid, true);
$data->parent = $top->id;
}
// 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.
@@ -4554,7 +4569,6 @@ class restore_create_categories_and_questions extends restore_structure_step {
'backupid' => $this->get_restoreid(),
'itemname' => 'question_category_created'));
foreach ($qcats as $qcat) {
$newparent = 0;
$dbcat = $DB->get_record('question_categories', array('id' => $qcat->newitemid));
// Get new parent (mapped or created, so we look in quesiton_category mappings)
if ($newparent = $DB->get_field('backup_ids_temp', 'newitemid', array(
@@ -4570,8 +4584,11 @@ class restore_create_categories_and_questions extends restore_structure_step {
}
}
// Here with $newparent empty, problem with contexts or remapping, set it to top cat
if (!$newparent) {
$DB->set_field('question_categories', 'parent', 0, array('id' => $dbcat->id));
if (!$newparent && $dbcat->parent) {
$topcat = question_get_top_category($dbcat->contextid, true);
if ($dbcat->parent != $topcat->id) {
$DB->set_field('question_categories', 'parent', $topcat->id, array('id' => $dbcat->id));
}
}
}
@@ -4580,7 +4597,6 @@ class restore_create_categories_and_questions extends restore_structure_step {
'backupid' => $this->get_restoreid(),
'itemname' => 'question_created'));
foreach ($qs as $q) {
$newparent = 0;
$dbq = $DB->get_record('question', array('id' => $q->newitemid));
// Get new parent (mapped or created, so we look in question mappings)
if ($newparent = $DB->get_field('backup_ids_temp', 'newitemid', array(
@@ -4609,18 +4625,38 @@ class restore_move_module_questions_categories extends restore_execution_step {
protected function define_execution() {
global $DB;
$backuprelease = floatval($this->task->get_info()->backup_release);
preg_match('/(\d{8})/', $this->task->get_info()->moodle_release, $matches);
$backupbuild = (int)$matches[1];
$before35 = false;
if ($backuprelease < 3.5 || $backupbuild < 20180205) {
$before35 = true;
}
$contexts = restore_dbops::restore_get_question_banks($this->get_restoreid(), CONTEXT_MODULE);
foreach ($contexts as $contextid => $contextlevel) {
// 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
$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));
foreach ($modulecats as $modulecat) {
$DB->set_field('question_categories', 'contextid', $newcontext->newitemid, array('id' => $modulecat->newitemid));
$cat = new stdClass();
$cat->id = $modulecat->newitemid;
$cat->contextid = $newcontext->newitemid;
// 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 ($before35 && empty($info->parent)) {
$top = question_get_top_category($newcontext->newitemid, true);
$cat->parent = $top->id;
}
$DB->update_record('question_categories', $cat);
// And set new contextid 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);
+32 -1
View File
@@ -558,9 +558,16 @@ abstract class restore_dbops {
*
* The function returns 2 arrays, one containing errors and another containing
* warnings. Both empty if no errors/warnings are found.
*
* @param int $restoreid The restore ID
* @param int $courseid The ID of the course
* @param int $userid The id of the user doing the restore
* @param bool $samesite True if restore is to same site
* @param int $contextlevel (CONTEXT_SYSTEM, etc.)
* @return array A separate list of all error and warnings detected
*/
public static function prechek_precheck_qbanks_by_level($restoreid, $courseid, $userid, $samesite, $contextlevel) {
global $CFG, $DB;
global $DB;
// To return any errors and warnings found
$errors = array();
@@ -571,6 +578,17 @@ abstract class restore_dbops {
CONTEXT_SYSTEM => CONTEXT_COURSE,
CONTEXT_COURSECAT => CONTEXT_COURSE);
$rc = restore_controller_dbops::load_controller($restoreid);
$restoreinfo = $rc->get_info();
$rc->destroy(); // Always need to destroy.
$backuprelease = floatval($restoreinfo->backup_release);
preg_match('/(\d{8})/', $restoreinfo->moodle_release, $matches);
$backupbuild = (int)$matches[1];
$after35 = false;
if ($backuprelease >= 3.5 && $backupbuild > 20180205) {
$after35 = true;
}
// For any contextlevel, follow this process logic:
//
// 0) Iterate over each context (qbank)
@@ -587,6 +605,7 @@ abstract class restore_dbops {
// 7a) There is fallback, move ALL the qcats to fallback, warn. End qcat loop
// 7b) No fallback, error. End qcat loop
// 5b) 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.
// Get all the contexts (question banks) in restore for the given contextlevel
$contexts = self::restore_get_question_banks($restoreid, $contextlevel);
@@ -596,6 +615,8 @@ abstract class restore_dbops {
// Init some perms
$canmanagecategory = false;
$canadd = false;
// Top-level category counter.
$topcats = 0;
// get categories in context (bank)
$categories = self::restore_get_question_categories($restoreid, $contextid);
// cache permissions if $targetcontext is found
@@ -605,6 +626,10 @@ abstract class restore_dbops {
}
// 1) Iterate over each qcat in the context, matching by stamp for the found target context
foreach ($categories as $category) {
if ($category->parent == 0) {
$topcats++;
}
$matchcat = false;
if ($targetcontext) {
$matchcat = $DB->get_record('question_categories', array(
@@ -690,6 +715,12 @@ 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.
if ($after35 && $topcats > 1) {
$errors[] = get_string('restoremultipletopcats', 'questions', $contextid);
}
}
return array($errors, $warnings);
@@ -26,7 +26,7 @@ require_once($CFG->dirroot.'/backup/util/xml/parser/processors/grouped_parser_pr
/**
* helper implementation of grouped_parser_processor that will
* load all the categories and questions (header info only) from then questions.xml file
* load all the categories and questions (header info only) from the questions.xml file
* to the backup_ids table storing the whole structure there for later processing.
* Note: only "needed" categories are loaded (must have question_categoryref record in backup_ids)
* Note: parentitemid will contain the category->contextid for categories
+3
View File
@@ -41,6 +41,8 @@ $string['cannotdeletecate'] = 'You can\'t delete that category it is the default
$string['cannotdeleteneededbehaviour'] = 'Cannot delete the question behaviour \'{$a}\'. There are other behaviours installed that rely on it.';
$string['cannotdeleteqtypeinuse'] = 'You cannot delete the question type \'{$a}\'. There are questions of this type in the question bank.';
$string['cannotdeleteqtypeneeded'] = 'You cannot delete the question type \'{$a}\'. There are other question types installed that rely on it.';
$string['cannotdeletetopcat'] = 'Top categories can not be deleted.';
$string['cannotedittopcat'] = 'Top categories can not be edited.';
$string['cannotenable'] = 'Question type {$a} cannot be created directly.';
$string['cannotenablebehaviour'] = 'Question behaviour {$a} cannot be used directly. It is for internal use only.';
$string['cannotfindcate'] = 'Could not find category record';
@@ -398,6 +400,7 @@ $string['requiresgrading'] = 'Requires grading';
$string['responsehistory'] = 'Response history';
$string['restart'] = 'Start again';
$string['restartwiththeseoptions'] = 'Start again with these options';
$string['restoremultipletopcats'] = 'The backup file contains more than one top-level question categories for context {$a}.';
$string['rightanswer'] = 'Right answer';
$string['rightanswer_help'] = 'an automatically generated summary of the correct response. This can be limited, so you may wish to consider explaining the correct solution in the general feedback for the question, and turning this option off.';
$string['saved'] = 'Saved: {$a}';
+38
View File
@@ -1935,5 +1935,43 @@ function xmldb_main_upgrade($oldversion) {
upgrade_main_savepoint(true, 2017122200.01);
}
if ($oldversion < 2018020500.00) {
$topcategory = new stdClass();
$topcategory->name = 'top'; // A non-real name for the top category. It will be localised at the display time.
$topcategory->info = '';
$topcategory->parent = 0;
$topcategory->sortorder = 0;
// Get the total record count - used for the progress bar.
$total = $DB->count_records_sql("SELECT COUNT(DISTINCT contextid) FROM {question_categories} WHERE parent = 0");
// Get the records themselves - a list of contextids.
$rs = $DB->get_recordset_sql("SELECT DISTINCT contextid FROM {question_categories} WHERE parent = 0");
// For each context, create a single top-level category.
$i = 0;
$pbar = new progress_bar('createtopquestioncategories', 500, true);
foreach ($rs as $contextid => $notused) {
$topcategory->contextid = $contextid;
$topcategory->stamp = make_unique_id_code();
$topcategoryid = $DB->insert_record('question_categories', $topcategory);
$DB->set_field_select('question_categories', 'parent', $topcategoryid,
'contextid = ? AND id <> ? AND parent = 0',
array($contextid, $topcategoryid));
// Update progress.
$i++;
$pbar->update($i, $total, "Creating top-level question categories - $i/$total.");
}
$rs->close();
// Main savepoint reached.
upgrade_main_savepoint(true, 2018020500.00);
}
return true;
}
+42
View File
@@ -6534,3 +6534,45 @@ function allow_switch($fromroleid, $targetroleid) {
core_role_set_switch_allowed($fromroleid, $targetroleid);
}
/**
* Organise categories into a single parent category (called the 'Top' category) per context.
*
* @param array $categories List of question categories in the format of ["$categoryid,$contextid" => $category].
* @param array $pcontexts List of context ids.
* @return array
* @deprecated since Moodle 3.5. MDL-61132
*/
function question_add_tops($categories, $pcontexts) {
debugging('question_add_tops() has been deprecated. You may want to pass $top = true to get_categories_for_contexts().',
DEBUG_DEVELOPER);
$topcats = array();
foreach ($pcontexts as $context) {
$topcat = question_get_top_category($context, true);
$newcat = new stdClass();
$newcat->id = "{$topcat->id},$context";
$newcat->name = get_string('top');
$newcat->parent = 0;
$newcat->contextid = $context;
$topcats["{$topcat->id},$context"] = $newcat;
}
// Put topcats in at beginning of array - they'll be sorted into different contexts later.
return array_merge($topcats, $categories);
}
/**
* Checks if the question category is the highest-level category in the context that can be edited, and has no siblings.
*
* @param int $categoryid a category id.
* @return bool
* @deprecated since Moodle 3.5. MDL-61132
*/
function question_is_only_toplevel_category_in_context($categoryid) {
debugging('question_is_only_toplevel_category_in_context() has been deprecated. '
. 'Please update your code to use question_is_only_child_of_top_category_in_context() instead.',
DEBUG_DEVELOPER);
return question_is_only_child_of_top_category_in_context($categoryid);
}
+79 -22
View File
@@ -971,7 +971,6 @@ function add_indented_names($categories, $nochildrenof = -1) {
*/
function question_category_select_menu($contexts, $top = false, $currentcat = 0,
$selected = "", $nochildrenof = -1) {
global $OUTPUT;
$categoriesarray = question_category_options($contexts, $top, $currentcat,
false, $nochildrenof);
if ($selected) {
@@ -994,8 +993,8 @@ function question_category_select_menu($contexts, $top = false, $currentcat = 0,
*/
function question_get_default_category($contextid) {
global $DB;
$category = $DB->get_records('question_categories',
array('contextid' => $contextid), 'id', '*', 0, 1);
$category = $DB->get_records_select('question_categories', 'contextid = ? AND parent <> 0',
array($contextid), 'id', '*', 0, 1);
if (!empty($category)) {
return reset($category);
} else {
@@ -1003,6 +1002,51 @@ function question_get_default_category($contextid) {
}
}
/**
* Gets the top category in the given context.
* This function can optionally create the top category if it doesn't exist.
*
* @param int $contextid A context id.
* @param bool $create Whether create a top category if it doesn't exist.
* @return bool|stdClass The top question category for that context, or false if none.
*/
function question_get_top_category($contextid, $create = false) {
global $DB;
$category = $DB->get_record('question_categories',
array('contextid' => $contextid, 'parent' => 0));
if (!$category && $create) {
// We need to make one.
$category = new stdClass();
$category->name = 'top'; // A non-real name for the top category. It will be localised at the display time.
$category->info = '';
$category->contextid = $contextid;
$category->parent = 0;
$category->sortorder = 0;
$category->stamp = make_unique_id_code();
$category->id = $DB->insert_record('question_categories', $category);
}
return $category;
}
/**
* Gets the list of top categories in the given contexts in the array("categoryid,categorycontextid") format.
*
* @param array $contextids List of context ids
* @return array
*/
function question_get_top_categories_for_contexts($contextids) {
global $DB;
$concatsql = $DB->sql_concat_join("','", ['id', 'contextid']);
list($insql, $params) = $DB->get_in_or_equal($contextids);
$sql = "SELECT $concatsql FROM {question_categories} WHERE contextid $insql AND parent = 0";
$topcategories = $DB->get_fieldset_sql($sql, $params);
return $topcategories;
}
/**
* Gets the default category in the most specific context.
* If no categories exist yet then default ones are created in all contexts.
@@ -1023,15 +1067,16 @@ function question_make_default_categories($contexts) {
$preferredness = 0;
// If it already exists, just return it.
foreach ($contexts as $key => $context) {
$topcategory = question_get_top_category($context->id, true);
if (!$exists = $DB->record_exists("question_categories",
array('contextid' => $context->id))) {
array('contextid' => $context->id, 'parent' => $topcategory->id))) {
// Otherwise, we need to make one
$category = new stdClass();
$contextname = $context->get_context_name(false, true);
$category->name = get_string('defaultfor', 'question', $contextname);
$category->info = get_string('defaultinfofor', 'question', $contextname);
$category->contextid = $context->id;
$category->parent = 0;
$category->parent = $topcategory->id;
// By default, all categories get this number, and are sorted alphabetically.
$category->sortorder = 999;
$category->stamp = make_unique_id_code();
@@ -1061,20 +1106,29 @@ function question_make_default_categories($contexts) {
*
* @param mixed $contexts either a single contextid, or a comma-separated list of context ids.
* @param string $sortorder used as the ORDER BY clause in the select statement.
* @param bool $top Whether to return the top categories or not.
* @return array of category objects.
*/
function get_categories_for_contexts($contexts, $sortorder = 'parent, sortorder, name ASC') {
function get_categories_for_contexts($contexts, $sortorder = 'parent, sortorder, name ASC', $top = false) {
global $DB;
$topwhere = $top ? '' : 'AND c.parent <> 0';
return $DB->get_records_sql("
SELECT c.*, (SELECT count(1) FROM {question} q
WHERE c.id = q.category AND q.hidden='0' AND q.parent='0') AS questioncount
FROM {question_categories} c
WHERE c.contextid IN ($contexts)
WHERE c.contextid IN ($contexts) $topwhere
ORDER BY $sortorder");
}
/**
* Output an array of question categories.
*
* @param array $contexts The list of contexts.
* @param bool $top Whether to return the top categories or not.
* @param int $currentcat
* @param bool $popupform
* @param int $nochildrenof
* @return array
*/
function question_category_options($contexts, $top = false, $currentcat = 0,
$popupform = false, $nochildrenof = -1) {
@@ -1085,13 +1139,13 @@ function question_category_options($contexts, $top = false, $currentcat = 0,
}
$contextslist = join($pcontexts, ', ');
$categories = get_categories_for_contexts($contextslist);
$categories = question_add_context_in_key($categories);
$categories = get_categories_for_contexts($contextslist, 'parent, sortorder, name ASC', $top);
if ($top) {
$categories = question_add_tops($categories, $pcontexts);
$categories = question_fix_top_names($categories);
}
$categories = question_add_context_in_key($categories);
$categories = add_indented_names($categories, $nochildrenof);
// sort cats out into different contexts
@@ -1138,18 +1192,21 @@ function question_add_context_in_key($categories) {
return $newcatarray;
}
function question_add_tops($categories, $pcontexts) {
$topcats = array();
foreach ($pcontexts as $context) {
$newcat = new stdClass();
$newcat->id = "0,$context";
$newcat->name = get_string('top');
$newcat->parent = -1;
$newcat->contextid = $context;
$topcats["0,$context"] = $newcat;
/**
* Finds top categories in the given categories hierarchy and replace their name with a proper localised string.
*
* @param array $categories An array of question categories.
* @return array The same question category list given to the function, with the top category names being translated.
*/
function question_fix_top_names($categories) {
foreach ($categories as $id => $category) {
if ($category->parent == 0) {
$categories[$id]->name = get_string('top');
}
}
//put topcats in at beginning of array - they'll be sorted into different contexts later.
return array_merge($topcats, $categories);
return $categories;
}
/**
+3 -2
View File
@@ -249,8 +249,8 @@ class core_questionlib_testcase extends advanced_testcase {
$rc->execute_plan();
// Get the created question category.
$restoredcategory = $DB->get_record('question_categories', array('contextid' => context_course::instance($course2->id)->id),
'*', MUST_EXIST);
$restoredcategory = $DB->get_record_select('question_categories', 'contextid = ? AND parent <> 0',
array(context_course::instance($course2->id)->id), '*', MUST_EXIST);
// Check that there are two questions in the restored to course's context.
$this->assertEquals(2, $DB->count_records('question', array('category' => $restoredcategory->id)));
@@ -334,6 +334,7 @@ class core_questionlib_testcase extends advanced_testcase {
$this->assertEquals(0, $DB->count_records('question', $criteria));
// Test that the feedback works.
$expected[] = array('top', get_string('unusedcategorydeleted', 'question'));
$expected[] = array($qcat->name, get_string('unusedcategorydeleted', 'question'));
$this->assertEquals($expected, $result);
}
+4
View File
@@ -85,6 +85,10 @@ if ($data = $mform->get_data()) {
if (!empty($data->existingcategory)) {
list($categoryid) = explode(',', $data->category);
$includesubcategories = !empty($data->includesubcategories);
if (!$includesubcategories) {
// If the chosen category is a top category.
$includesubcategories = $DB->record_exists('question_categories', ['id' => $categoryid, 'parent' => 0]);
}
$returnurl->param('cat', $data->category);
} else if (!empty($data->newcategory)) {
+4 -2
View File
@@ -37,7 +37,6 @@ require_once($CFG->libdir.'/formslib.php');
class quiz_add_random_form extends moodleform {
protected function definition() {
global $CFG, $DB;
$mform =& $this->_form;
$mform->setDisableShortforms();
@@ -49,11 +48,14 @@ class quiz_add_random_form extends moodleform {
get_string('randomfromexistingcategory', 'quiz'));
$mform->addElement('questioncategory', 'category', get_string('category'),
array('contexts' => $usablecontexts, 'top' => false));
array('contexts' => $usablecontexts, 'top' => true));
$mform->setDefault('category', $this->_customdata['cat']);
$mform->addElement('checkbox', 'includesubcategories', '', get_string('recurse', 'quiz'));
$tops = question_get_top_categories_for_contexts(array_column($contexts->all(), 'id'));
$mform->hideIf('includesubcategories', 'category', 'in', $tops);
$mform->addElement('select', 'numbertoadd', get_string('randomnumber', 'quiz'),
$this->get_number_of_questions_to_add_choices());
+10 -8
View File
@@ -112,8 +112,8 @@ class question_category_list_item extends list_item {
$item .= format_text($category->info, $category->infoformat,
array('context' => $this->parentlist->context, 'noclean' => true));
// don't allow delete if this is the last category in this context.
if (!question_is_only_toplevel_category_in_context($category->id)) {
// Don't allow delete if this is the top category, or the last editable category in this context.
if ($category->parent && !question_is_only_child_of_top_category_in_context($category->id)) {
$deleteurl = new moodle_url($this->parentlist->pageurl, array('delete' => $this->id, 'sesskey' => sesskey()));
$item .= html_writer::link($deleteurl,
$OUTPUT->pix_icon('t/delete', $str->delete),
@@ -295,17 +295,19 @@ class question_category_object {
public function edit_single_category($categoryid) {
/// Interface for adding a new category
global $COURSE, $DB;
global $DB;
/// Interface for editing existing categories
if ($category = $DB->get_record("question_categories", array("id" => $categoryid))) {
$category = $DB->get_record("question_categories", array("id" => $categoryid));
if (empty($category)) {
print_error('invalidcategory', '', '', $categoryid);
} else if ($category->parent == 0) {
print_error('cannotedittopcat', 'question', '', $categoryid);
} else {
$category->parent = "{$category->parent},{$category->contextid}";
$category->submitbutton = get_string('savechanges');
$category->categoryheader = $this->str->edit;
$this->catform->set_data($category);
$this->catform->display();
} else {
print_error('invalidcategory', '', '', $categoryid);
}
}
@@ -440,7 +442,7 @@ class question_category_object {
// Get the record we are updating.
$oldcat = $DB->get_record('question_categories', array('id' => $updateid));
$lastcategoryinthiscontext = question_is_only_toplevel_category_in_context($updateid);
$lastcategoryinthiscontext = question_is_only_child_of_top_category_in_context($updateid);
if (!empty($newparent) && !$lastcategoryinthiscontext) {
list($parentid, $tocontextid) = explode(',', $newparent);
+3 -4
View File
@@ -38,7 +38,6 @@ require_once($CFG->libdir.'/formslib.php');
class question_category_edit_form extends moodleform {
protected function definition() {
global $CFG, $DB;
$mform = $this->_form;
$contexts = $this->_customdata['contexts'];
@@ -46,10 +45,10 @@ class question_category_edit_form extends moodleform {
$mform->addElement('header', 'categoryheader', get_string('addcategory', 'question'));
$questioncategoryel = $mform->addElement('questioncategory', 'parent', get_string('parentcategory', 'question'),
array('contexts'=>$contexts, 'top'=>true, 'currentcat'=>$currentcat, 'nochildrenof'=>$currentcat));
$mform->addElement('questioncategory', 'parent', get_string('parentcategory', 'question'),
array('contexts' => $contexts, 'top' => true, 'currentcat' => $currentcat, 'nochildrenof' => $currentcat));
$mform->setType('parent', PARAM_SEQUENCE);
if (question_is_only_toplevel_category_in_context($currentcat)) {
if (question_is_only_child_of_top_category_in_context($currentcat)) {
$mform->hardFreeze('parent');
}
$mform->addHelpButton('parent', 'parentcategory', 'question');
@@ -130,12 +130,11 @@ class category_condition extends condition {
* @param string $current 'categoryID,contextID'.
*/
protected function display_category_form($contexts, $pageurl, $current) {
global $OUTPUT;
echo \html_writer::start_div('choosecategory');
$catmenu = question_category_options($contexts, false, 0, true);
$catmenu = question_category_options($contexts, true, 0, true);
echo \html_writer::label(get_string('selectacategory', 'question'), 'id_selectacategory');
echo \html_writer::select($catmenu, 'category', $current, array(), array('class' => 'searchoptions custom-select', 'id' => 'id_selectacategory'));
echo \html_writer::select($catmenu, 'category', $current, array(),
array('class' => 'searchoptions custom-select', 'id' => 'id_selectacategory'));
echo \html_writer::end_div() . "\n";
}
+23 -9
View File
@@ -95,28 +95,42 @@ function get_questions_category( $category, $noparent=false, $recurse=true, $exp
}
/**
* Checks whether this is the only child of a top category in a context.
*
* @param int $categoryid a category id.
* @return bool whether this is the only top-level category in a context.
* @return bool
*/
function question_is_only_toplevel_category_in_context($categoryid) {
function question_is_only_child_of_top_category_in_context($categoryid) {
global $DB;
return 1 == $DB->count_records_sql("
SELECT count(*)
FROM {question_categories} c1,
{question_categories} c2
WHERE c2.id = ?
AND c1.contextid = c2.contextid
AND c1.parent = 0 AND c2.parent = 0", array($categoryid));
FROM {question_categories} c
JOIN {question_categories} p ON c.parent = p.id
JOIN {question_categories} s ON s.parent = c.parent
WHERE c.id = ? AND p.parent = 0", array($categoryid));
}
/**
* Check whether this user is allowed to delete this category.
* Checks whether the category is a "Top" category (with no parent).
*
* @param int $categoryid a category id.
* @return bool
*/
function question_is_top_category($categoryid) {
global $DB;
return 0 == $DB->get_field('question_categories', 'parent', array('id' => $categoryid));
}
/**
* Ensures that this user is allowed to delete this category.
*
* @param int $todelete a category id.
*/
function question_can_delete_cat($todelete) {
global $DB;
if (question_is_only_toplevel_category_in_context($todelete)) {
if (question_is_top_category($todelete)) {
print_error('cannotdeletetopcat', 'question');
} else if (question_is_only_child_of_top_category_in_context($todelete)) {
print_error('cannotdeletecate', 'question');
} else {
$contextid = $DB->get_field('question_categories', 'contextid', array('id' => $todelete));
+2 -1
View File
@@ -69,7 +69,8 @@ class question_export_form extends moodleform {
// Export options.
$mform->addElement('header', 'general', get_string('general', 'form'));
$mform->addElement('questioncategory', 'category', get_string('exportcategory', 'question'), compact('contexts'));
$mform->addElement('questioncategory', 'category', get_string('exportcategory', 'question'),
array('contexts' => $contexts, 'top' => true));
$mform->setDefault('category', $defaultcategory);
$mform->addHelpButton('category', 'exportcategory', 'question');
+13 -1
View File
@@ -499,6 +499,12 @@ class qformat_default {
$contextid = false;
}
// 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.
if (isset($catnames[0]) && (($catnames[0] != 'top') || (count($catnames) < 3))) {
array_unshift($catnames, 'top');
}
if ($this->contextfromfile && $contextid !== false) {
$context = context::instance_by_id($contextid);
require_capability('moodle/question:add', $context);
@@ -509,9 +515,15 @@ class qformat_default {
// Now create any categories that need to be created.
foreach ($catnames as $catname) {
if ($category = $DB->get_record('question_categories',
if ($parent == 0) {
$category = question_get_top_category($context->id, true);
$parent = $category->id;
} else if ($category = $DB->get_record('question_categories',
array('name' => $catname, 'contextid' => $context->id, 'parent' => $parent))) {
$parent = $category->id;
} else if ($parent == 0) {
$category = question_get_top_category($context->id, true);
$parent = $category->id;
} else {
require_capability('moodle/question:managecategory', $context);
// create the new category
+1 -1
View File
@@ -46,4 +46,4 @@ Feature: A teacher can duplicate questions in the question bank
When I click on "Duplicate" "link" in the "Test question to be copied" "table_row"
And I press "Cancel"
Then I should see "Test question to be copied"
And the field "Select a category" matches value "Test questions (1)"
And the field "Select a category" matches value "&nbsp;&nbsp;&nbsp;Test questions (1)"
@@ -1,4 +1,4 @@
@core @core_question
@core @core_question @javascript
Feature: A teacher can put questions in categories in the question bank
In order to organize my questions
As a teacher
@@ -16,9 +16,10 @@ Feature: A teacher can put questions in categories in the question bank
| teacher1 | C1 | editingteacher |
And the following "question categories" exist:
| contextlevel | reference | questioncategory | name |
| Course | C1 | Top | Default for C1 |
| Course | C1 | Top | top |
| Course | C1 | top | Default for C1 |
| Course | C1 | Default for C1 | Subcategory |
| Course | C1 | Top | Used category |
| Course | C1 | top | Used category |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Used category | essay | Test question to be moved | Write about whatever you want |
@@ -38,7 +39,7 @@ Feature: A teacher can put questions in categories in the question bank
Scenario: A question category can be edited
When I navigate to "Categories" node in "Course administration > Question bank"
And I click on "Edit" "link" in the "Subcategory" "list_item"
And the field "parent" matches value "   Default for C1"
And the field "parent" matches value "&nbsp;&nbsp;&nbsp;Default for C1"
And I set the following fields to these values:
| Name | New name |
| Category info | I was edited |
@@ -67,7 +68,7 @@ Feature: A teacher can put questions in categories in the question bank
And I set the field "Question category" to "Subcategory"
And I press "Move to >>"
Then I should see "Test question to be moved"
And the field "Select a category" matches value "&nbsp;&nbsp;&nbsp;Subcategory (1)"
And the field "Select a category" matches value "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Subcategory (1)"
And the "Select a category" select box should contain "Used category"
And the "Select a category" select box should not contain "Used category (1)"
@@ -80,6 +81,6 @@ Feature: A teacher can put questions in categories in the question bank
And I set the field "Save in category" to "Subcategory"
And I press "id_submitbutton"
Then I should see "Test question to be moved"
And the field "Select a category" matches value "&nbsp;&nbsp;&nbsp;Subcategory (1)"
And the field "Select a category" matches value "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Subcategory (1)"
And the "Select a category" select box should contain "Used category"
And the "Select a category" select box should not contain "Used category (1)"
+7 -2
View File
@@ -47,15 +47,20 @@ class core_question_generator extends component_generator_base {
$defaults = array(
'name' => 'Test question category ' . $this->categorycount,
'contextid' => context_system::instance()->id,
'info' => '',
'infoformat' => FORMAT_HTML,
'stamp' => make_unique_id_code(),
'parent' => 0,
'sortorder' => 999,
);
$record = $this->datagenerator->combine_defaults_and_record($defaults, $record);
if (!isset($record['contextid'])) {
$record['contextid'] = context_system::instance()->id;
}
if (!isset($record['parent'])) {
$record['parent'] = question_get_top_category($record['contextid'], true)->id;
}
$record['id'] = $DB->insert_record('question_categories', $record);
return (object) $record;
}
+3 -1
View File
@@ -42,7 +42,9 @@ class core_question_generator_testcase extends advanced_testcase {
$count = $DB->count_records('question_categories');
$cat = $generator->create_question_category();
$this->assertEquals($count + 1, $DB->count_records('question_categories'));
$count += $count ? 1 : 2; // Calling $generator->create_question_category() for the first time
// creates a Top category as well.
$this->assertEquals($count, $DB->count_records('question_categories'));
$cat = $generator->create_question_category(array(
'name' => 'My category', 'sortorder' => 1));
+6
View File
@@ -732,6 +732,12 @@ abstract class question_edit_form extends question_wizard_form {
$errors['currentgrp'] = get_string('nopermissionmove', 'question');
}
// Category.
if (empty($fromform['category'])) {
// User has provided an invalid category.
$errors['category'] = get_string('required');
}
// Default mark.
if (array_key_exists('defaultmark', $fromform) && $fromform['defaultmark'] < 0) {
$errors['defaultmark'] = get_string('defaultmarkmustbepositive', 'question');
+4 -1
View File
@@ -48,11 +48,14 @@ class qtype_random_edit_form extends question_edit_form {
$mform->addElement('header', 'generalheader', get_string("general", 'form'));
$mform->addElement('questioncategory', 'category', get_string('category', 'question'),
array('contexts' => $this->contexts->having_cap('moodle/question:useall')));
array('contexts' => $this->contexts->having_cap('moodle/question:useall'), 'top' => true));
$mform->addElement('advcheckbox', 'questiontext[text]',
get_string('includingsubcategories', 'qtype_random'), null, null, array(0, 1));
$tops = question_get_top_categories_for_contexts(array_column($this->contexts->all(), 'id'));
$mform->hideIf('questiontext[text]', 'category', 'in', $tops);
$mform->addElement('hidden', 'qtype');
$mform->setType('qtype', PARAM_ALPHA);
@@ -29,6 +29,11 @@ $string['pluginname'] = 'Random';
$string['pluginname_help'] = 'A random question is not a question type as such, but is a way of inserting a randomly-chosen question from a specified category into an activity.';
$string['pluginnameediting'] = 'Editing a random question';
$string['randomqname'] = 'Random ({$a})';
$string['randomqnamefromtop'] = 'Faulty random question! Please delete this question.';
$string['randomqplusname'] = 'Random ({$a} and subcategories)';
$string['randomqplusnamecourse'] = 'Random (Any category in this course)';
$string['randomqplusnamecoursecat'] = 'Random (Any category inside course category {$a})';
$string['randomqplusnamemodule'] = 'Random (Any category of this quiz)';
$string['randomqplusnamesystem'] = 'Random (Any system-level category)';
$string['selectedby'] = '{$a->questionname} selected by {$a->randomname}';
$string['selectmanualquestions'] = 'Random questions can use manually graded questions';
+34 -4
View File
@@ -127,12 +127,36 @@ class qtype_random extends question_type {
* @return string the name this question should have.
*/
public function question_name($category, $includesubcategories) {
if ($includesubcategories) {
$string = 'randomqplusname';
if ($category->parent && $includesubcategories) {
$name = get_string('randomqplusname', 'qtype_random', shorten_text($category->name, 100));
} else if ($category->parent) {
$name = get_string('randomqname', 'qtype_random', shorten_text($category->name, 100));
} else if ($includesubcategories) {
$context = context::instance_by_id($category->contextid);
switch ($context->contextlevel) {
case CONTEXT_MODULE:
$name = get_string('randomqplusnamemodule', 'qtype_random');
break;
case CONTEXT_COURSE:
$name = get_string('randomqplusnamecourse', 'qtype_random');
break;
case CONTEXT_COURSECAT:
$name = get_string('randomqplusnamecoursecat', 'qtype_random',
shorten_text($context->get_context_name(false), 100));
break;
case CONTEXT_SYSTEM:
$name = get_string('randomqplusnamesystem', 'qtype_random');
break;
default: // Impossible.
$name = '';
}
} else {
$string = 'randomqname';
// No question will ever be selected. So, let's warn the teacher.
$name = get_string('randomqnamefromtop', 'qtype_random');
}
return get_string($string, 'qtype_random', shorten_text($category->name, 100));
return $name;
}
protected function set_selected_question_name($question, $randomname) {
@@ -143,11 +167,17 @@ class qtype_random extends question_type {
}
public function save_question($question, $form) {
global $DB;
$form->name = '';
list($category) = explode(',', $form->category);
// In case someone set the question text to true/false in the old style, set it properly.
if ($form->questiontext['text']) {
$form->questiontext['text'] = '1';
} else if ($DB->record_exists('question_categories', ['id' => $category, 'parent' => 0])) {
// The chosen category is a top category.
$form->questiontext['text'] = '1';
} else {
$form->questiontext['text'] = '0';
}
+2 -2
View File
@@ -29,11 +29,11 @@
defined('MOODLE_INTERNAL') || die();
$version = 2018020100.01; // YYYYMMDD = weekly release date of this DEV branch.
$version = 2018020600.00; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.
$release = '3.5dev (Build: 20180201)'; // Human-friendly version name
$release = '3.5dev (Build: 20180205)'; // Human-friendly version name
$branch = '35'; // This version's branch.
$maturity = MATURITY_ALPHA; // This version's maturity level.