Merge branch 'master_MDL-71585-managecategories' of https://github.com/catalyst/moodle-MDL-70329

This commit is contained in:
Sara Arjona 2021-09-06 10:39:06 +02:00
commit 3b903ae45d
51 changed files with 2471 additions and 703 deletions

View File

@ -60,7 +60,7 @@ abstract class question_base extends base {
['courseid' => $this->courseid, 'cat' => $cat, 'lastchanged' => $this->objectid]);
}
// Lets try viewing from the frontpage for contexts above course.
return new \moodle_url('/question/category.php',
return new \moodle_url('/question/bank/managecategories/category.php',
['courseid' => SITEID, 'edit' => $this->other['categoryid'], 'lastchanged' => $this->objectid]);
}

View File

@ -58,7 +58,7 @@ abstract class question_category_base extends base {
return new \moodle_url('/question/edit.php', ['courseid' => $this->courseid, 'cat' => $cat]);
}
// Lets try viewing from the frontpage for contexts above course.
return new \moodle_url('/question/category.php', ['courseid' => SITEID, 'edit' => $this->objectid]);
return new \moodle_url('/question/bank/managecategories/category.php', ['courseid' => SITEID, 'edit' => $this->objectid]);
}
/**

View File

@ -87,7 +87,7 @@ class question_moved extends question_base {
['courseid' => $this->courseid, 'cat' => $cat, 'lastchanged' => $this->objectid]);
}
// Lets try viewing from the frontpage for contexts above course.
return new \moodle_url('/question/category.php',
return new \moodle_url('/question/bank/managecategories/category.php',
['courseid' => SITEID, 'edit' => $this->other['newcategoryid'], 'lastchanged' => $this->objectid]);
}

View File

@ -83,7 +83,8 @@ class questions_exported extends question_base {
}
return new \moodle_url('/question/edit.php', ['courseid' => $this->courseid, 'cat' => $cat]);
}
return new \moodle_url('/question/category.php', ['courseid' => SITEID, 'edit' => $this->other['categoryid']]);
return new \moodle_url('/question/bank/managecategories/category.php',
['courseid' => SITEID, 'edit' => $this->other['categoryid']]);
}
/**

View File

@ -83,7 +83,8 @@ class questions_imported extends question_base {
}
return new \moodle_url('/question/edit.php', ['courseid' => $this->courseid, 'cat' => $cat]);
}
return new \moodle_url('/question/category.php', ['courseid' => SITEID, 'edit' => $this->other['categoryid']]);
return new \moodle_url('/question/bank/managecategories/category.php',
['courseid' => SITEID, 'edit' => $this->other['categoryid']]);
}
/**

View File

@ -1944,6 +1944,7 @@ class core_plugin_manager {
'exporttoxml',
'exportquestions',
'importquestions',
'managecategories',
'viewcreator',
'viewquestionname',
'viewquestiontext',

View File

@ -26,6 +26,7 @@
*/
global $CFG;
use qbank_managecategories\helper;
require_once("$CFG->libdir/form/selectgroups.php");
require_once("$CFG->libdir/questionlib.php");
@ -59,8 +60,8 @@ class MoodleQuickForm_questioncategory extends MoodleQuickForm_selectgroups {
if (is_array($options)) {
$this->_options = $options + $this->_options;
$this->loadArrayOptGroups(
question_category_options($this->_options['contexts'], $this->_options['top'], $this->_options['currentcat'],
false, $this->_options['nochildrenof'], false));
helper::question_category_options($this->_options['contexts'], $this->_options['top'],
$this->_options['currentcat'], false, $this->_options['nochildrenof'], false));
}
}

View File

@ -238,18 +238,16 @@ function match_grade_options($gradeoptionsfull, $grade, $matchgrades = 'error')
* - random questions
*
* @param int $categoryid The category ID.
* @deprecated since Moodle 4.0 MDL-71585
* @see qbank_managecategories\helper
* @todo Final deprecation on Moodle 4.4 MDL-72438
*/
function question_remove_stale_questions_from_category($categoryid) {
global $DB;
$select = 'category = :categoryid AND (qtype = :qtype OR hidden = :hidden)';
$params = ['categoryid' => $categoryid, 'qtype' => 'random', 'hidden' => 1];
$questions = $DB->get_recordset_select("question", $select, $params, '', 'id');
foreach ($questions as $question) {
// The function question_delete_question does not delete questions in use.
question_delete_question($question->id);
}
$questions->close();
debugging('Function question_remove_stale_questions_from_category()
has been deprecated and moved to qbank_managecategories plugin,
Please use qbank_managecategories\helper::question_remove_stale_questions_from_category() instead.',
DEBUG_DEVELOPER);
\qbank_managecategories\helper::question_remove_stale_questions_from_category($categoryid);
}
/**
@ -1146,27 +1144,14 @@ function sort_categories_by_tree(&$categories, $id = 0, $level = 1) {
* @param int $id the category to start the indenting process from.
* @param int $depth the indent depth. Used in recursive calls.
* @return array a new array of categories, in the right order for the tree.
* @deprecated since Moodle 4.0 MDL-71585
* @see qbank_managecategories\helper
* @todo Final deprecation on Moodle 4.4 MDL-72438
*/
function flatten_category_tree(&$categories, $id, $depth = 0, $nochildrenof = -1) {
// Indent the name of this category.
$newcategories = array();
$newcategories[$id] = $categories[$id];
$newcategories[$id]->indentedname = str_repeat('   ', $depth) .
$categories[$id]->name;
// Recursively indent the children.
foreach ($categories[$id]->childids as $childid) {
if ($childid != $nochildrenof) {
$newcategories = $newcategories + flatten_category_tree(
$categories, $childid, $depth + 1, $nochildrenof);
}
}
// Remove the childids array that were temporarily added.
unset($newcategories[$id]->childids);
return $newcategories;
debugging('Function flatten_category_tree() has been deprecated and moved to qbank_managecategories plugin,
Please use qbank_managecategories\helper::flatten_category_tree() instead.', DEBUG_DEVELOPER);
return \qbank_managecategories\helper::flatten_category_tree($categories, $id, $depth, $nochildrenof);
}
/**
@ -1174,37 +1159,14 @@ function flatten_category_tree(&$categories, $id, $depth = 0, $nochildrenof = -1
*
* @param array $categories An array of category objects, for example from the.
* @return array The formatted list of categories.
* @deprecated since Moodle 4.0 MDL-71585
* @see qbank_managecategories\helper
* @todo Final deprecation on Moodle 4.4 MDL-72438
*/
function add_indented_names($categories, $nochildrenof = -1) {
// Add an array to each category to hold the child category ids. This array
// will be removed again by flatten_category_tree(). It should not be used
// outside these two functions.
foreach (array_keys($categories) as $id) {
$categories[$id]->childids = array();
}
// Build the tree structure, and record which categories are top-level.
// We have to be careful, because the categories array may include published
// categories from other courses, but not their parents.
$toplevelcategoryids = array();
foreach (array_keys($categories) as $id) {
if (!empty($categories[$id]->parent) &&
array_key_exists($categories[$id]->parent, $categories)) {
$categories[$categories[$id]->parent]->childids[] = $id;
} else {
$toplevelcategoryids[] = $id;
}
}
// Flatten the tree to and add the indents.
$newcategories = array();
foreach ($toplevelcategoryids as $id) {
$newcategories = $newcategories + flatten_category_tree(
$categories, $id, 0, $nochildrenof);
}
return $newcategories;
debugging('Function add_indented_names() has been deprecated and moved to qbank_managecategories plugin,
Please use qbank_managecategories\helper::add_indented_names() instead.', DEBUG_DEVELOPER);
return \qbank_managecategories\helper::add_indented_names($categories, $nochildrenof);
}
/**
@ -1218,30 +1180,15 @@ function add_indented_names($categories, $nochildrenof = -1) {
* @param integer $only_editable if true, exclude categories this user is not allowed to edit.
* @param integer $selected optionally, the id of a category to be selected by
* default in the dropdown.
* @deprecated since Moodle 4.0 MDL-71585
* @see qbank_managecategories\helper
* @todo Final deprecation on Moodle 4.4 MDL-72438
*/
function question_category_select_menu($contexts, $top = false, $currentcat = 0,
$selected = "", $nochildrenof = -1) {
$categoriesarray = question_category_options($contexts, $top, $currentcat,
false, $nochildrenof, false);
if ($selected) {
$choose = '';
} else {
$choose = 'choosedots';
}
$options = array();
foreach ($categoriesarray as $group => $opts) {
$options[] = array($group => $opts);
}
echo html_writer::label(get_string('questioncategory', 'core_question'), 'id_movetocategory', false, array('class' => 'accesshide'));
$attrs = array(
'id' => 'id_movetocategory',
'class' => 'custom-select',
'data-action' => 'toggle',
'data-togglegroup' => 'qbank',
'data-toggle' => 'action',
'disabled' => true,
);
echo html_writer::select($options, 'category', $selected, $choose, $attrs);
debugging('Function question_category_select_menu() has been deprecated and moved to qbank_managecategories plugin,
Please use qbank_managecategories\helper::question_category_select_menu() instead.', DEBUG_DEVELOPER);
\qbank_managecategories\helper::question_category_select_menu($contexts, $top, $currentcat, $selected, $nochildrenof);
}
/**
@ -1366,16 +1313,14 @@ function question_make_default_categories($contexts) {
* @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.
* @deprecated since Moodle 4.0 MDL-71585
* @see qbank_managecategories\helper
* @todo Final deprecation on Moodle 4.4 MDL-72438
*/
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) $topwhere
ORDER BY $sortorder");
debugging('Function get_categories_for_contexts() has been deprecated and moved to qbank_managecategories plugin,
Please use qbank_managecategories\helper::get_categories_for_contexts() instead.', DEBUG_DEVELOPER);
return \qbank_managecategories\helper::get_categories_for_contexts($contexts, $sortorder, $top);
}
/**
@ -1388,81 +1333,27 @@ function get_categories_for_contexts($contexts, $sortorder = 'parent, sortorder,
* @param int $nochildrenof
* @param boolean $escapecontextnames Whether the returned name of the thing is to be HTML escaped or not.
* @return array
* @deprecated since Moodle 4.0 MDL-71585
* @see qbank_managecategories\helper
* @todo Final deprecation on Moodle 4.4 MDL-72438
*/
function question_category_options($contexts, $top = false, $currentcat = 0,
$popupform = false, $nochildrenof = -1, $escapecontextnames = true) {
global $CFG;
$pcontexts = array();
foreach ($contexts as $context) {
$pcontexts[] = $context->id;
}
$contextslist = join(', ', $pcontexts);
$categories = get_categories_for_contexts($contextslist, 'parent, sortorder, name ASC', $top);
if ($top) {
$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
$categoriesarray = array();
foreach ($pcontexts as $contextid) {
$context = context::instance_by_id($contextid);
$contextstring = $context->get_context_name(true, true, $escapecontextnames);
foreach ($categories as $category) {
if ($category->contextid == $contextid) {
$cid = $category->id;
if ($currentcat != $cid || $currentcat == 0) {
$a = new stdClass;
$a->name = format_string($category->indentedname, true,
array('context' => $context));
if ($category->idnumber !== null && $category->idnumber !== '') {
$a->idnumber = s($category->idnumber);
}
if (!empty($category->questioncount)) {
$a->questioncount = $category->questioncount;
}
if (isset($a->idnumber) && isset($a->questioncount)) {
$formattedname = get_string('categorynamewithidnumberandcount', 'question', $a);
} else if (isset($a->idnumber)) {
$formattedname = get_string('categorynamewithidnumber', 'question', $a);
} else if (isset($a->questioncount)) {
$formattedname = get_string('categorynamewithcount', 'question', $a);
} else {
$formattedname = $a->name;
}
$categoriesarray[$contextstring][$cid] = $formattedname;
}
}
}
}
if ($popupform) {
$popupcats = array();
foreach ($categoriesarray as $contextstring => $optgroup) {
$group = array();
foreach ($optgroup as $key => $value) {
$key = str_replace($CFG->wwwroot, '', $key);
$group[$key] = $value;
}
$popupcats[] = array($contextstring => $group);
}
return $popupcats;
} else {
return $categoriesarray;
}
debugging('Function question_category_options() has been deprecated and moved to qbank_managecategories plugin,
Please use qbank_managecategories\helper::question_category_options() instead.', DEBUG_DEVELOPER);
return \qbank_managecategories\helper::question_category_options($contexts, $top, $currentcat,
$popupform, $nochildrenof, $escapecontextnames);
}
/**
* @deprecated since Moodle 4.0 MDL-71585
* @see qbank_managecategories\helper
* @todo Final deprecation on Moodle 4.4 MDL-72438
*/
function question_add_context_in_key($categories) {
$newcatarray = array();
foreach ($categories as $id => $category) {
$category->parent = "$category->parent,$category->contextid";
$category->id = "$category->id,$category->contextid";
$newcatarray["$id,$category->contextid"] = $category;
}
return $newcatarray;
debugging('Function question_add_context_in_key() has been deprecated and moved to qbank_managecategories plugin,
Please use qbank_managecategories\helper::question_add_context_in_key() instead.', DEBUG_DEVELOPER);
return \qbank_managecategories\helper::question_add_context_in_key($categories);
}
/**
@ -1471,17 +1362,14 @@ function question_add_context_in_key($categories) {
* @param array $categories An array of question categories.
* @param boolean $escape Whether the returned name of the thing is to be HTML escaped or not.
* @return array The same question category list given to the function, with the top category names being translated.
* @deprecated since Moodle 4.0 MDL-71585
* @see qbank_managecategories\helper
* @todo Final deprecation on Moodle 4.4 MDL-72438
*/
function question_fix_top_names($categories, $escape = true) {
foreach ($categories as $id => $category) {
if ($category->parent == 0) {
$context = context::instance_by_id($category->contextid);
$categories[$id]->name = get_string('topfor', 'question', $context->get_context_name(false, false, $escape));
}
}
return $categories;
debugging('Function question_fix_top_names() has been deprecated and moved to qbank_managecategories plugin,
Please use qbank_managecategories\helper::question_fix_top_names() instead.', DEBUG_DEVELOPER);
return \qbank_managecategories\helper::question_fix_top_names($categories, $escape);
}
/**

View File

@ -517,68 +517,6 @@ class core_questionlib_testcase extends advanced_testcase {
$this->assertEquals(1, $DB->count_records('question_categories', ['contextid' => $qcat->contextid, 'parent' => 0]));
}
public function test_question_remove_stale_questions_from_category() {
global $DB;
$this->resetAfterTest(true);
$this->setAdminUser();
$dg = $this->getDataGenerator();
$course = $dg->create_course();
$quiz = $dg->create_module('quiz', ['course' => $course->id]);
$qgen = $dg->get_plugin_generator('core_question');
$context = context_system::instance();
$qcat1 = $qgen->create_question_category(['contextid' => $context->id]);
$q1a = $qgen->create_question('shortanswer', null, ['category' => $qcat1->id]); // Will be hidden.
$DB->set_field('question', 'hidden', 1, ['id' => $q1a->id]);
$qcat2 = $qgen->create_question_category(['contextid' => $context->id]);
$q2a = $qgen->create_question('shortanswer', null, ['category' => $qcat2->id]); // Will be hidden.
$q2b = $qgen->create_question('shortanswer', null, ['category' => $qcat2->id]); // Will be hidden but used.
$DB->set_field('question', 'hidden', 1, ['id' => $q2a->id]);
$DB->set_field('question', 'hidden', 1, ['id' => $q2b->id]);
quiz_add_quiz_question($q2b->id, $quiz);
quiz_add_random_questions($quiz, 0, $qcat2->id, 1, false);
// We added one random question to the quiz and we expect the quiz to have only one random question.
$q2d = $DB->get_record_sql("SELECT q.*
FROM {question} q
JOIN {quiz_slots} s ON s.questionid = q.id
WHERE q.qtype = :qtype
AND s.quizid = :quizid",
array('qtype' => 'random', 'quizid' => $quiz->id), MUST_EXIST);
// The following 2 lines have to be after the quiz_add_random_questions() call above.
// Otherwise, quiz_add_random_questions() will to be "smart" and use them instead of creating a new "random" question.
$q1b = $qgen->create_question('random', null, ['category' => $qcat1->id]); // Will not be used.
$q2c = $qgen->create_question('random', null, ['category' => $qcat2->id]); // Will not be used.
$this->assertEquals(2, $DB->count_records('question', ['category' => $qcat1->id]));
$this->assertEquals(4, $DB->count_records('question', ['category' => $qcat2->id]));
// Non-existing category, nothing will happen.
question_remove_stale_questions_from_category(0);
$this->assertEquals(2, $DB->count_records('question', ['category' => $qcat1->id]));
$this->assertEquals(4, $DB->count_records('question', ['category' => $qcat2->id]));
// First category, should be empty afterwards.
question_remove_stale_questions_from_category($qcat1->id);
$this->assertEquals(0, $DB->count_records('question', ['category' => $qcat1->id]));
$this->assertEquals(4, $DB->count_records('question', ['category' => $qcat2->id]));
$this->assertFalse($DB->record_exists('question', ['id' => $q1a->id]));
$this->assertFalse($DB->record_exists('question', ['id' => $q1b->id]));
// Second category, used questions should be left untouched.
question_remove_stale_questions_from_category($qcat2->id);
$this->assertEquals(0, $DB->count_records('question', ['category' => $qcat1->id]));
$this->assertEquals(2, $DB->count_records('question', ['category' => $qcat2->id]));
$this->assertFalse($DB->record_exists('question', ['id' => $q2a->id]));
$this->assertTrue($DB->record_exists('question', ['id' => $q2b->id]));
$this->assertFalse($DB->record_exists('question', ['id' => $q2c->id]));
$this->assertTrue($DB->record_exists('question', ['id' => $q2d->id]));
}
/**
* get_question_options should add the category object to the given question.
*/

View File

@ -69,6 +69,17 @@ information provided here is intended especially for developers.
rendered. The default icon for "select" types has also changed to a dropdown caret ("t/expanded").
* The function message_send() in messagelib.php now returns false if there is an error sending the message to the
message processor (MDL-70046).
* The following functions are deprecated in questionlib.php and moved to the new location.
These are marked for final deprecation on 4.4:
- question_remove_stale_questions_from_category() =>
qbank_managecategories\helper::question_remove_stale_questions_from_category()
- flatten_category_tree() => qbank_managecategories\helper::flatten_category_tree()
- add_indented_names() => qbank_managecategories\helper::add_indented_names()
- question_category_select_menu() => qbank_managecategories\helper::question_category_select_menu()
- get_categories_for_contexts() => qbank_managecategories\helper::get_categories_for_contexts()
- question_category_options() => qbank_managecategories\helper::question_category_options()
- question_add_context_in_key() => qbank_managecategories\helper::question_add_context_in_key()
- question_fix_top_names() => qbank_managecategories\helper::question_fix_top_names()
=== 3.11.2 ===
* For security reasons, filelib has been updated so all requests now use emulated redirects.

View File

@ -28,7 +28,8 @@ require_once(__DIR__ . '/../../config.php');
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
require_once($CFG->dirroot . '/mod/quiz/addrandomform.php');
require_once($CFG->dirroot . '/question/editlib.php');
require_once($CFG->dirroot . '/question/category_class.php');
use qbank_managecategories\question_category_object;
list($thispageurl, $contexts, $cmid, $cm, $quiz, $pagevars) =
question_edit_setup('editq', '/mod/quiz/addrandom.php', true);

View File

@ -85,19 +85,22 @@ class quiz_add_random_form extends moodleform {
$mform->addElement('submit', 'existingcategory', get_string('addrandomquestion', 'quiz'));
// Random from a new category section.
$mform->addElement('header', 'newcategoryheader',
get_string('randomquestionusinganewcategory', 'quiz'));
// If the manage categories plugins is enabled, add the elements to create a new category in the form.
if (\core\plugininfo\qbank::is_plugin_enabled(\qbank_managecategories\helper::PLUGINNAME)) {
// Random from a new category section.
$mform->addElement('header', 'newcategoryheader',
get_string('randomquestionusinganewcategory', 'quiz'));
$mform->addElement('text', 'name', get_string('name'), 'maxlength="254" size="50"');
$mform->setType('name', PARAM_TEXT);
$mform->addElement('text', 'name', get_string('name'), 'maxlength="254" size="50"');
$mform->setType('name', PARAM_TEXT);
$mform->addElement('questioncategory', 'parent', get_string('parentcategory', 'question'),
array('contexts' => $usablecontexts, 'top' => true));
$mform->addHelpButton('parent', 'parentcategory', 'question');
$mform->addElement('questioncategory', 'parent', get_string('parentcategory', 'question'),
array('contexts' => $usablecontexts, 'top' => true));
$mform->addHelpButton('parent', 'parentcategory', 'question');
$mform->addElement('submit', 'newcategory',
get_string('createcategoryandaddrandomquestion', 'quiz'));
$mform->addElement('submit', 'newcategory',
get_string('createcategoryandaddrandomquestion', 'quiz'));
}
// Cancel button.
$mform->addElement('cancel');

View File

@ -1,2 +1,2 @@
define ("mod_quiz/add_question_modal_launcher",["jquery","core/notification","core/modal_factory"],function(a,b,c){return{init:function init(d,e,f,g){var h=a("body");return c.create({type:d,large:!0,preShowCallback:function preShowCallback(b,c){b=a(b);c.setContextId(f);c.setAddOnPageId(b.attr("data-addonpage"));c.setTitle(b.attr("data-header"));if(g){g(b,c)}}},[h,e]).fail(b.exception)}}});
define ("mod_quiz/add_question_modal_launcher",["jquery","core/notification","core/modal_factory"],function(a,b,c){return{init:function init(d,e,f,g){var h=4<arguments.length&&arguments[4]!==void 0?arguments[4]:!0,i=a("body");return c.create({type:d,large:!0,templateContext:{hidden:h},preShowCallback:function preShowCallback(b,c){b=a(b);c.setContextId(f);c.setAddOnPageId(b.attr("data-addonpage"));c.setTitle(b.attr("data-header"));if(g){g(b,c)}}},[i,e]).fail(b.exception)}}});
//# sourceMappingURL=add_question_modal_launcher.min.js.map

View File

@ -1 +1 @@
{"version":3,"sources":["../src/add_question_modal_launcher.js"],"names":["define","$","Notification","ModalFactory","init","modalType","selector","contextId","preShowCallback","body","create","type","large","triggerElement","modal","setContextId","setAddOnPageId","attr","setTitle","fail","exception"],"mappings":"AAsBAA,OAAM,wCACF,CACI,QADJ,CAEI,mBAFJ,CAGI,oBAHJ,CADE,CAMF,SACIC,CADJ,CAEIC,CAFJ,CAGIC,CAHJ,CAIE,CAEF,MAAO,CAWHC,IAAI,CAAE,cAASC,CAAT,CAAoBC,CAApB,CAA8BC,CAA9B,CAAyCC,CAAzC,CAA0D,CAC5D,GAAIC,CAAAA,CAAI,CAAGR,CAAC,CAAC,MAAD,CAAZ,CAOA,MAAOE,CAAAA,CAAY,CAACO,MAAb,CACH,CACIC,IAAI,CAAEN,CADV,CAEIO,KAAK,GAFT,CAKIJ,eAAe,CAAE,yBAASK,CAAT,CAAyBC,CAAzB,CAAgC,CAC7CD,CAAc,CAAGZ,CAAC,CAACY,CAAD,CAAlB,CACAC,CAAK,CAACC,YAAN,CAAmBR,CAAnB,EACAO,CAAK,CAACE,cAAN,CAAqBH,CAAc,CAACI,IAAf,CAAoB,gBAApB,CAArB,EACAH,CAAK,CAACI,QAAN,CAAeL,CAAc,CAACI,IAAf,CAAoB,aAApB,CAAf,EAEA,GAAIT,CAAJ,CAAqB,CACjBA,CAAe,CAACK,CAAD,CAAiBC,CAAjB,CAClB,CACJ,CAdL,CADG,CAmBH,CAACL,CAAD,CAAOH,CAAP,CAnBG,EAoBLa,IApBK,CAoBAjB,CAAY,CAACkB,SApBb,CAqBV,CAxCE,CA0CV,CAtDK,CAAN","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Initialise the an add question modal on the quiz page.\n *\n * @module mod_quiz/add_question_modal_launcher\n * @copyright 2018 Ryan Wyllie <ryan@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(\n [\n 'jquery',\n 'core/notification',\n 'core/modal_factory',\n ],\n function(\n $,\n Notification,\n ModalFactory\n ) {\n\n return {\n /**\n * Create a modal using the modal factory and add listeners to launch the\n * modal when clicked.\n *\n * @param {string} modalType Which modal to create\n * @param {string} selector The selectors for the elements that trigger the modal\n * @param {int} contextId The current context id\n * @param {function} preShowCallback A callback to execute before the modal is shown\n * @return {promise} Resolved with the modal\n */\n init: function(modalType, selector, contextId, preShowCallback) {\n var body = $('body');\n\n // Create a question bank modal using the factory.\n // The same modal will be used by all of the add question\n // links that match \"selector\" on the page. The content\n // of the modal will be changed depending on which link is\n // clicked.\n return ModalFactory.create(\n {\n type: modalType,\n large: true,\n // This callback executes before the modal is shown when the\n // trigger element is clicked.\n preShowCallback: function(triggerElement, modal) {\n triggerElement = $(triggerElement);\n modal.setContextId(contextId);\n modal.setAddOnPageId(triggerElement.attr('data-addonpage'));\n modal.setTitle(triggerElement.attr('data-header'));\n\n if (preShowCallback) {\n preShowCallback(triggerElement, modal);\n }\n }\n },\n // Created a deligated listener rather than a single\n // trigger element.\n [body, selector]\n ).fail(Notification.exception);\n }\n };\n});\n"],"file":"add_question_modal_launcher.min.js"}
{"version":3,"sources":["../src/add_question_modal_launcher.js"],"names":["define","$","Notification","ModalFactory","init","modalType","selector","contextId","preShowCallback","showNewCategory","body","create","type","large","templateContext","hidden","triggerElement","modal","setContextId","setAddOnPageId","attr","setTitle","fail","exception"],"mappings":"AAsBAA,OAAM,wCACF,CACI,QADJ,CAEI,mBAFJ,CAGI,oBAHJ,CADE,CAMF,SACIC,CADJ,CAEIC,CAFJ,CAGIC,CAHJ,CAIE,CAEF,MAAO,CAYHC,IAAI,CAAE,cAASC,CAAT,CAAoBC,CAApB,CAA8BC,CAA9B,CAAyCC,CAAzC,CAAkF,IAAxBC,CAAAA,CAAwB,2DAChFC,CAAI,CAAGT,CAAC,CAAC,MAAD,CADwE,CAWpF,MAAOE,CAAAA,CAAY,CAACQ,MAAb,CACH,CACIC,IAAI,CAAEP,CADV,CAEIQ,KAAK,GAFT,CAGIC,eAAe,CAbD,CAClBC,MAAM,CAAEN,CADU,CAUlB,CAMID,eAAe,CAAE,yBAASQ,CAAT,CAAyBC,CAAzB,CAAgC,CAC7CD,CAAc,CAAGf,CAAC,CAACe,CAAD,CAAlB,CACAC,CAAK,CAACC,YAAN,CAAmBX,CAAnB,EACAU,CAAK,CAACE,cAAN,CAAqBH,CAAc,CAACI,IAAf,CAAoB,gBAApB,CAArB,EACAH,CAAK,CAACI,QAAN,CAAeL,CAAc,CAACI,IAAf,CAAoB,aAApB,CAAf,EAEA,GAAIZ,CAAJ,CAAqB,CACjBA,CAAe,CAACQ,CAAD,CAAiBC,CAAjB,CAClB,CACJ,CAfL,CADG,CAoBH,CAACP,CAAD,CAAOJ,CAAP,CApBG,EAqBLgB,IArBK,CAqBApB,CAAY,CAACqB,SArBb,CAsBV,CA7CE,CA+CV,CA3DK,CAAN","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Initialise the an add question modal on the quiz page.\n *\n * @module mod_quiz/add_question_modal_launcher\n * @copyright 2018 Ryan Wyllie <ryan@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(\n [\n 'jquery',\n 'core/notification',\n 'core/modal_factory',\n ],\n function(\n $,\n Notification,\n ModalFactory\n ) {\n\n return {\n /**\n * Create a modal using the modal factory and add listeners to launch the\n * modal when clicked.\n *\n * @param {string} modalType Which modal to create\n * @param {string} selector The selectors for the elements that trigger the modal\n * @param {int} contextId The current context id\n * @param {function} preShowCallback A callback to execute before the modal is shown\n * @param {boolean} showNewCategory Display the New category tab when selecting random questions.\n * @return {promise} Resolved with the modal\n */\n init: function(modalType, selector, contextId, preShowCallback, showNewCategory = true) {\n var body = $('body');\n let templateContext = {\n hidden: showNewCategory,\n };\n\n // Create a question bank modal using the factory.\n // The same modal will be used by all of the add question\n // links that match \"selector\" on the page. The content\n // of the modal will be changed depending on which link is\n // clicked.\n return ModalFactory.create(\n {\n type: modalType,\n large: true,\n templateContext: templateContext,\n // This callback executes before the modal is shown when the\n // trigger element is clicked.\n preShowCallback: function(triggerElement, modal) {\n triggerElement = $(triggerElement);\n modal.setContextId(contextId);\n modal.setAddOnPageId(triggerElement.attr('data-addonpage'));\n modal.setTitle(triggerElement.attr('data-header'));\n\n if (preShowCallback) {\n preShowCallback(triggerElement, modal);\n }\n }\n },\n // Created a deligated listener rather than a single\n // trigger element.\n [body, selector]\n ).fail(Notification.exception);\n }\n };\n});\n"],"file":"add_question_modal_launcher.min.js"}

View File

@ -1,2 +1,2 @@
define ("mod_quiz/add_random_question",["mod_quiz/add_question_modal_launcher","mod_quiz/modal_add_random_question"],function(a,b){return{init:function init(c,d,e,f){a.init(b.TYPE,".menu [data-action=\"addarandomquestion\"]",c,function(a,b){b.setCategory(d);b.setReturnUrl(e);b.setCMID(f)})}}});
define ("mod_quiz/add_random_question",["mod_quiz/add_question_modal_launcher","mod_quiz/modal_add_random_question"],function(a,b){return{init:function init(c,d,e,f){var g=4<arguments.length&&arguments[4]!==void 0?arguments[4]:!0;a.init(b.TYPE,".menu [data-action=\"addarandomquestion\"]",c,function(a,b){b.setCategory(d);b.setReturnUrl(e);b.setCMID(f)},g)}}});
//# sourceMappingURL=add_random_question.min.js.map

View File

@ -1 +1 @@
{"version":3,"sources":["../src/add_random_question.js"],"names":["define","AddQuestionModalLauncher","ModalAddRandomQuestion","init","contextId","category","returnUrl","cmid","TYPE","triggerElement","modal","setCategory","setReturnUrl","setCMID"],"mappings":"AAsBAA,OAAM,gCACF,CACI,sCADJ,CAEI,oCAFJ,CADE,CAKF,SACIC,CADJ,CAEIC,CAFJ,CAGE,CAEF,MAAO,CASHC,IAAI,CAAE,cAASC,CAAT,CAAoBC,CAApB,CAA8BC,CAA9B,CAAyCC,CAAzC,CAA+C,CACjDN,CAAwB,CAACE,IAAzB,CACID,CAAsB,CAACM,IAD3B,CAEI,4CAFJ,CAGIJ,CAHJ,CAKI,SAASK,CAAT,CAAyBC,CAAzB,CAAgC,CAC5BA,CAAK,CAACC,WAAN,CAAkBN,CAAlB,EACAK,CAAK,CAACE,YAAN,CAAmBN,CAAnB,EACAI,CAAK,CAACG,OAAN,CAAcN,CAAd,CACH,CATL,CAWH,CArBE,CAuBV,CAjCK,CAAN","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Initialise the add random question modal on the quiz page.\n *\n * @module mod_quiz/add_random_question\n * @copyright 2018 Ryan Wyllie <ryan@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(\n [\n 'mod_quiz/add_question_modal_launcher',\n 'mod_quiz/modal_add_random_question'\n ],\n function(\n AddQuestionModalLauncher,\n ModalAddRandomQuestion\n ) {\n\n return {\n /**\n * Create the add random question modal.\n *\n * @param {int} contextId Current context id.\n * @param {string} category Category id and category context id comma separated.\n * @param {string} returnUrl URL to return to after form submission.\n * @param {int} cmid Current course module id.\n */\n init: function(contextId, category, returnUrl, cmid) {\n AddQuestionModalLauncher.init(\n ModalAddRandomQuestion.TYPE,\n '.menu [data-action=\"addarandomquestion\"]',\n contextId,\n // Additional values that should be set before the modal is shown.\n function(triggerElement, modal) {\n modal.setCategory(category);\n modal.setReturnUrl(returnUrl);\n modal.setCMID(cmid);\n }\n );\n }\n };\n});\n"],"file":"add_random_question.min.js"}
{"version":3,"sources":["../src/add_random_question.js"],"names":["define","AddQuestionModalLauncher","ModalAddRandomQuestion","init","contextId","category","returnUrl","cmid","showNewCategory","TYPE","triggerElement","modal","setCategory","setReturnUrl","setCMID"],"mappings":"AAsBAA,OAAM,gCACF,CACI,sCADJ,CAEI,oCAFJ,CADE,CAKF,SACIC,CADJ,CAEIC,CAFJ,CAGE,CAEF,MAAO,CAUHC,IAAI,CAAE,cAASC,CAAT,CAAoBC,CAApB,CAA8BC,CAA9B,CAAyCC,CAAzC,CAAuE,IAAxBC,CAAAA,CAAwB,2DACzEP,CAAwB,CAACE,IAAzB,CACID,CAAsB,CAACO,IAD3B,CAEI,4CAFJ,CAGIL,CAHJ,CAKI,SAASM,CAAT,CAAyBC,CAAzB,CAAgC,CAC5BA,CAAK,CAACC,WAAN,CAAkBP,CAAlB,EACAM,CAAK,CAACE,YAAN,CAAmBP,CAAnB,EACAK,CAAK,CAACG,OAAN,CAAcP,CAAd,CACH,CATL,CAUIC,CAVJ,CAYH,CAvBE,CAyBV,CAnCK,CAAN","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Initialise the add random question modal on the quiz page.\n *\n * @module mod_quiz/add_random_question\n * @copyright 2018 Ryan Wyllie <ryan@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(\n [\n 'mod_quiz/add_question_modal_launcher',\n 'mod_quiz/modal_add_random_question'\n ],\n function(\n AddQuestionModalLauncher,\n ModalAddRandomQuestion\n ) {\n\n return {\n /**\n * Create the add random question modal.\n *\n * @param {int} contextId Current context id.\n * @param {string} category Category id and category context id comma separated.\n * @param {string} returnUrl URL to return to after form submission.\n * @param {int} cmid Current course module id.\n * @param {boolean} showNewCategory Display the New category tab when selecting random questions.\n */\n init: function(contextId, category, returnUrl, cmid, showNewCategory = true) {\n AddQuestionModalLauncher.init(\n ModalAddRandomQuestion.TYPE,\n '.menu [data-action=\"addarandomquestion\"]',\n contextId,\n // Additional values that should be set before the modal is shown.\n function(triggerElement, modal) {\n modal.setCategory(category);\n modal.setReturnUrl(returnUrl);\n modal.setCMID(cmid);\n },\n showNewCategory\n );\n }\n };\n});\n"],"file":"add_random_question.min.js"}

View File

@ -41,10 +41,14 @@ define(
* @param {string} selector The selectors for the elements that trigger the modal
* @param {int} contextId The current context id
* @param {function} preShowCallback A callback to execute before the modal is shown
* @param {boolean} showNewCategory Display the New category tab when selecting random questions.
* @return {promise} Resolved with the modal
*/
init: function(modalType, selector, contextId, preShowCallback) {
init: function(modalType, selector, contextId, preShowCallback, showNewCategory = true) {
var body = $('body');
let templateContext = {
hidden: showNewCategory,
};
// Create a question bank modal using the factory.
// The same modal will be used by all of the add question
@ -55,6 +59,7 @@ define(
{
type: modalType,
large: true,
templateContext: templateContext,
// This callback executes before the modal is shown when the
// trigger element is clicked.
preShowCallback: function(triggerElement, modal) {

View File

@ -38,8 +38,9 @@ define(
* @param {string} category Category id and category context id comma separated.
* @param {string} returnUrl URL to return to after form submission.
* @param {int} cmid Current course module id.
* @param {boolean} showNewCategory Display the New category tab when selecting random questions.
*/
init: function(contextId, category, returnUrl, cmid) {
init: function(contextId, category, returnUrl, cmid, showNewCategory = true) {
AddQuestionModalLauncher.init(
ModalAddRandomQuestion.TYPE,
'.menu [data-action="addarandomquestion"]',
@ -49,7 +50,8 @@ define(
modal.setCategory(category);
modal.setReturnUrl(returnUrl);
modal.setCMID(cmid);
}
},
showNewCategory
);
}
};

View File

@ -119,7 +119,8 @@ class edit_renderer extends \plugin_renderer_base {
$thiscontext->id,
$pagevars['cat'],
$pageurl->out_as_local_url(true),
$pageurl->param('cmid')
$pageurl->param('cmid'),
\core\plugininfo\qbank::is_plugin_enabled(\qbank_managecategories\helper::PLUGINNAME),
]);
// Include the question chooser.

View File

@ -45,7 +45,6 @@ require_once(__DIR__ . '/../../config.php');
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
require_once($CFG->dirroot . '/mod/quiz/addrandomform.php');
require_once($CFG->dirroot . '/question/editlib.php');
require_once($CFG->dirroot . '/question/category_class.php');
// These params are only passed from page request to request while we stay on
// this page otherwise they would go in question_edit_setup.

View File

@ -47,6 +47,7 @@
{{#str}} existingcategory, mod_quiz {{/str}}
</a>
</li>
{{#hidden}}
<li class="nav-item">
<a class="nav-link"
data-toggle="tab"
@ -55,6 +56,7 @@
{{#str}} newcategory, mod_quiz {{/str}}
</a>
</li>
{{/hidden}}
</ul>
<div class="tab-content" data-region="tab-content">
<div class="text-sm-center pt-5" data-region="loading-container">
@ -65,11 +67,13 @@
role="tabpanel"
data-region="existing-category-container">
</div>
{{#hidden}}
<div class="tab-pane pt-3"
id="new-category-{{uniqid}}"
role="tabpanel"
data-region="new-category-container">
</div>
{{/hidden}}
</div>
{{/body}}
{{/ core/modal }}

View File

@ -17,19 +17,24 @@
/**
* This script allows a teacher to create, edit and delete question categories.
*
* @package moodlecore
* @subpackage questionbank
* @package qbank_managecategories
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
* @author 2021, Guillermo Gomez Arias <guillermogomez@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once("../config.php");
require_once(__DIR__ . '/../../../config.php');
require_once($CFG->dirroot."/question/editlib.php");
require_once($CFG->dirroot."/question/category_class.php");
use qbank_managecategories\form\question_move_form;
use qbank_managecategories\helper;
use qbank_managecategories\question_category_object;
require_login();
core_question\local\bank\helper::require_plugin_enabled(helper::PLUGINNAME);
list($thispageurl, $contexts, $cmid, $cm, $module, $pagevars) =
question_edit_setup('categories', '/question/category.php');
question_edit_setup('categories', '/question/bank/managecategories/category.php');
// Get values from form for actions on this page.
$param = new stdClass();
@ -48,7 +53,7 @@ $param->moveto = optional_param('moveto', 0, PARAM_INT);
$param->edit = optional_param('edit', 0, PARAM_INT);
$url = new moodle_url($thispageurl);
foreach ((array)$param as $key=>$value) {
foreach ((array)$param as $key => $value) {
if (($key !== 'cancel' && $value !== 0) || ($key === 'cancel' && $value !== '')) {
$url->param($key, $value);
}
@ -78,9 +83,9 @@ if ($param->moveupcontext || $param->movedowncontext) {
}
$newtopcat = question_get_top_category($param->tocontext);
if (!$newtopcat) {
print_error('invalidcontext');
throw new moodle_exception('invalidcontext');
}
$oldcat = $DB->get_record('question_categories', array('id' => $catid), '*', MUST_EXIST);
$oldcat = $DB->get_record('question_categories', ['id' => $catid], '*', MUST_EXIST);
// Log the move to another context.
$category = new stdClass();
$category->id = explode(',', $pagevars['cat'], -1)[0];
@ -92,18 +97,18 @@ if ($param->moveupcontext || $param->movedowncontext) {
}
if ($param->delete) {
if (!$category = $DB->get_record("question_categories", array("id" => $param->delete))) {
print_error('nocate', 'question', $thispageurl->out(), $param->delete);
if (!$category = $DB->get_record("question_categories", ["id" => $param->delete])) {
throw new moodle_exception('nocate', 'question', $thispageurl->out(), $param->delete);
}
question_remove_stale_questions_from_category($param->delete);
$questionstomove = $DB->count_records("question", array("category" => $param->delete));
helper::question_remove_stale_questions_from_category($param->delete);
$questionstomove = $DB->count_records("question", ["category" => $param->delete]);
// Second pass, if we still have questions to move, setup the form.
if ($questionstomove) {
$categorycontext = context::instance_by_id($category->contextid);
$qcobject->moveform = new question_move_form($thispageurl,
array('contexts' => array($categorycontext), 'currentcat' => $param->delete));
['contexts' => [$categorycontext], 'currentcat' => $param->delete]);
if ($qcobject->moveform->is_cancelled()) {
redirect($thispageurl);
} else if ($formdata = $qcobject->moveform->get_data()) {
@ -122,7 +127,7 @@ if ($qcobject->catform->is_cancelled()) {
} else if ($catformdata = $qcobject->catform->get_data()) {
$catformdata->infoformat = $catformdata->info['format'];
$catformdata->info = $catformdata->info['text'];
if (!$catformdata->id) {//new category
if (!$catformdata->id) {// New category.
$qcobject->add_category($catformdata->parent, $catformdata->name,
$catformdata->info, false, $catformdata->infoformat, $catformdata->idnumber);
} else {
@ -131,7 +136,7 @@ if ($qcobject->catform->is_cancelled()) {
}
redirect($thispageurl);
} else if ((!empty($param->delete) and (!$questionstomove) and confirm_sesskey())) {
$qcobject->delete_category($param->delete);//delete the category now no questions to move
$qcobject->delete_category($param->delete);// Delete the category now no questions to move.
$thispageurl->remove_params('cat', 'category');
redirect($thispageurl);
}
@ -151,7 +156,7 @@ echo $renderer->extra_horizontal_navigation();
// Display the UI.
if (!empty($param->edit)) {
$qcobject->edit_single_category($param->edit);
} else if ($questionstomove){
} else if ($questionstomove) {
$qcobject->display_move_form($questionstomove, $category);
} else {
// Display the user interface.

View File

@ -0,0 +1,127 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace qbank_managecategories\form;
use moodleform;
use qbank_managecategories\helper;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir.'/formslib.php');
/**
* Defines the form for editing question categories.
*
* Form for editing questions categories (name, description, etc.)
*
* @package qbank_managecategories
* @copyright 2007 Jamie Pratt me@jamiep.org
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class question_category_edit_form extends moodleform {
/**
* Build the form definition.
*
* This adds all the form fields that the manage categories feature needs.
* @throws \coding_exception
*/
protected function definition() {
$mform = $this->_form;
$contexts = $this->_customdata['contexts'];
$currentcat = $this->_customdata['currentcat'];
$mform->addElement('header', 'categoryheader', get_string('addcategory', 'question'));
$mform->addElement('questioncategory', 'parent', get_string('parentcategory', 'question'),
['contexts' => $contexts, 'top' => true, 'currentcat' => $currentcat, 'nochildrenof' => $currentcat]);
$mform->setType('parent', PARAM_SEQUENCE);
if (helper::question_is_only_child_of_top_category_in_context($currentcat)) {
$mform->hardFreeze('parent');
}
$mform->addHelpButton('parent', 'parentcategory', 'question');
$mform->addElement('text', 'name', get_string('name'), 'maxlength="254" size="50"');
$mform->setDefault('name', '');
$mform->addRule('name', get_string('categorynamecantbeblank', 'question'), 'required', null, 'client');
$mform->setType('name', PARAM_TEXT);
$mform->addElement('editor', 'info', get_string('categoryinfo', 'question'),
['rows' => 10], ['noclean' => 1]);
$mform->setDefault('info', '');
$mform->setType('info', PARAM_RAW);
$mform->addElement('text', 'idnumber', get_string('idnumber', 'question'), 'maxlength="100" size="10"');
$mform->addHelpButton('idnumber', 'idnumber', 'question');
$mform->setType('idnumber', PARAM_RAW);
$this->add_action_buttons(false, get_string('addcategory', 'question'));
$mform->addElement('hidden', 'id', 0);
$mform->setType('id', PARAM_INT);
}
/**
* Set data method.
*
* Add additional information to current data.
* @param \stdClass|array $current Object or array of default current data.
*/
public function set_data($current) {
if (is_object($current)) {
$current = (array) $current;
}
if (!empty($current['info'])) {
$current['info'] = ['text' => $current['info'], 'infoformat' => $current['infoformat']];
} else {
$current['info'] = ['text' => '', 'infoformat' => FORMAT_HTML];
}
parent::set_data($current);
}
/**
* Validation.
*
* @param array $data
* @param array $files
* @return array the errors that were found
* @throws \dml_exception|\coding_exception
*/
public function validation($data, $files) {
global $DB;
$errors = parent::validation($data, $files);
// Add field validation check for duplicate idnumber.
list($parentid, $contextid) = explode(',', $data['parent']);
if (((string) $data['idnumber'] !== '') && !empty($contextid)) {
$conditions = 'contextid = ? AND idnumber = ?';
$params = [$contextid, $data['idnumber']];
if (!empty($data['id'])) {
$conditions .= ' AND id <> ?';
$params[] = $data['id'];
}
if ($DB->record_exists_select('question_categories', $conditions, $params)) {
$errors['idnumber'] = get_string('idnumbertaken', 'error');
}
}
return $errors;
}
}

View File

@ -0,0 +1,54 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace qbank_managecategories\form;
use moodleform;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . '/formslib.php');
/**
* Form for moving questions between categories.
*
* @package qbank_managecategories
* @copyright 2008 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class question_move_form extends moodleform {
/**
* Build the form definition.
*
* This adds all the form fields that the question move feature needs.
* @throws \coding_exception
*/
protected function definition() {
$mform = $this->_form;
$currentcat = $this->_customdata['currentcat'];
$contexts = $this->_customdata['contexts'];
$mform->addElement('questioncategory', 'category', get_string('category', 'question'), compact('contexts', 'currentcat'));
$this->add_action_buttons(true, get_string('categorymoveto', 'question'));
$mform->addElement('hidden', 'delete', $currentcat);
$mform->setType('delete', PARAM_INT);
}
}

View File

@ -0,0 +1,373 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace qbank_managecategories;
use context;
use moodle_exception;
use html_writer;
/**
* Class helper contains all the library functions.
*
* Library functions used by qbank_managecategories.
* This code is based on lib/questionlib.php by Martin Dougiamas.
*
* @package qbank_managecategories
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Guillermo Gomez Arias <guillermogomez@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class helper {
/**
* Name of this plugin.
*/
const PLUGINNAME = 'qbank_managecategories';
/**
* Remove stale questions from a category.
*
* While questions should not be left behind when they are not used any more,
* it does happen, maybe via restore, or old logic, or uncovered scenarios. When
* this happens, the users are unable to delete the question category unless
* they move those stale questions to another one category, but to them the
* category is empty as it does not contain anything. The purpose of this function
* is to detect the questions that may have gone stale and remove them.
*
* You will typically use this prior to checking if the category contains questions.
*
* The stale questions (unused and hidden to the user) handled are:
* - hidden questions
* - random questions
*
* @param int $categoryid The category ID.
* @throws \dml_exception
*/
public static function question_remove_stale_questions_from_category(int $categoryid): void {
global $DB;
$select = 'category = :categoryid AND (qtype = :qtype OR hidden = :hidden)';
$params = ['categoryid' => $categoryid, 'qtype' => 'random', 'hidden' => 1];
$questions = $DB->get_recordset_select("question", $select, $params, '', 'id');
foreach ($questions as $question) {
// The function question_delete_question does not delete questions in use.
question_delete_question($question->id);
}
$questions->close();
}
/**
* Checks whether this is the only child of a top category in a context.
*
* @param int $categoryid a category id.
* @return bool
* @throws \dml_exception
*/
public static function question_is_only_child_of_top_category_in_context(int $categoryid): bool {
global $DB;
return 1 == $DB->count_records_sql("
SELECT count(*)
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", [$categoryid]);
}
/**
* Checks whether the category is a "Top" category (with no parent).
*
* @param int $categoryid a category id.
* @return bool
* @throws \dml_exception
*/
public static function question_is_top_category(int $categoryid): bool {
global $DB;
return 0 == $DB->get_field('question_categories', 'parent', ['id' => $categoryid]);
}
/**
* Ensures that this user is allowed to delete this category.
*
* @param int $todelete a category id.
* @throws \required_capability_exception
* @throws \dml_exception|moodle_exception
*/
public static function question_can_delete_cat(int $todelete): void {
global $DB;
if (self::question_is_top_category($todelete)) {
throw new moodle_exception('cannotdeletetopcat', 'question');
} else if (self::question_is_only_child_of_top_category_in_context($todelete)) {
throw new moodle_exception('cannotdeletecate', 'question');
} else {
$contextid = $DB->get_field('question_categories', 'contextid', ['id' => $todelete]);
require_capability('moodle/question:managecategory', context::instance_by_id($contextid));
}
}
/**
* Only for the use of add_indented_names().
*
* Recursively adds an indentedname field to each category, starting with the category
* with id $id, and dealing with that category and all its children, and
* return a new array, with those categories in the right order.
*
* @param array $categories an array of categories which has had childids
* fields added by flatten_category_tree(). Passed by reference for
* performance only. It is not modfied.
* @param int $id the category to start the indenting process from.
* @param int $depth the indent depth. Used in recursive calls.
* @param int $nochildrenof
* @return array a new array of categories, in the right order for the tree.
*/
public static function flatten_category_tree(array &$categories, $id, int $depth = 0, int $nochildrenof = -1): array {
// Indent the name of this category.
$newcategories = [];
$newcategories[$id] = $categories[$id];
$newcategories[$id]->indentedname = str_repeat('&nbsp;&nbsp;&nbsp;', $depth) .
$categories[$id]->name;
// Recursively indent the children.
foreach ($categories[$id]->childids as $childid) {
if ($childid != $nochildrenof) {
$newcategories = $newcategories + self::flatten_category_tree(
$categories, $childid, $depth + 1, $nochildrenof);
}
}
// Remove the childids array that were temporarily added.
unset($newcategories[$id]->childids);
return $newcategories;
}
/**
* Format categories into an indented list reflecting the tree structure.
*
* @param array $categories An array of category objects, for example from the.
* @param int $nochildrenof
* @return array The formatted list of categories.
*/
public static function add_indented_names(array $categories, int $nochildrenof = -1): array {
// Add an array to each category to hold the child category ids. This array
// will be removed again by flatten_category_tree(). It should not be used
// outside these two functions.
foreach (array_keys($categories) as $id) {
$categories[$id]->childids = [];
}
// Build the tree structure, and record which categories are top-level.
// We have to be careful, because the categories array may include published
// categories from other courses, but not their parents.
$toplevelcategoryids = [];
foreach (array_keys($categories) as $id) {
if (!empty($categories[$id]->parent) &&
array_key_exists($categories[$id]->parent, $categories)) {
$categories[$categories[$id]->parent]->childids[] = $id;
} else {
$toplevelcategoryids[] = $id;
}
}
// Flatten the tree to and add the indents.
$newcategories = [];
foreach ($toplevelcategoryids as $id) {
$newcategories = $newcategories + self::flatten_category_tree(
$categories, $id, 0, $nochildrenof);
}
return $newcategories;
}
/**
* Output a select menu of question categories.
*
* Categories from this course and (optionally) published categories from other courses
* are included. Optionally, only categories the current user may edit can be included.
*
* @param array $contexts
* @param bool $top
* @param int $currentcat
* @param string $selected optionally, the id of a category to be selected by
* default in the dropdown.
* @param int $nochildrenof
* @throws \coding_exception|\dml_exception
*/
public static function question_category_select_menu(array $contexts, bool $top = false, int $currentcat = 0,
string $selected = "", int $nochildrenof = -1): void {
$categoriesarray = self::question_category_options($contexts, $top, $currentcat,
false, $nochildrenof, false);
if ($selected) {
$choose = '';
} else {
$choose = 'choosedots';
}
$options = [];
foreach ($categoriesarray as $group => $opts) {
$options[] = [$group => $opts];
}
echo html_writer::label(get_string('questioncategory', 'core_question'),
'id_movetocategory', false, ['class' => 'accesshide']);
$attrs = [
'id' => 'id_movetocategory',
'class' => 'custom-select',
'data-action' => 'toggle',
'data-togglegroup' => 'qbank',
'data-toggle' => 'action',
'disabled' => true,
];
echo html_writer::select($options, 'category', $selected, $choose, $attrs);
}
/**
* Get all the category objects, including a count of the number of questions in that category,
* for all the categories in the lists $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.
* @throws \dml_exception
*/
public static function get_categories_for_contexts($contexts, string $sortorder = 'parent, sortorder, name ASC',
bool $top = false): array {
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) $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
* @param bool $escapecontextnames Whether the returned name of the thing is to be HTML escaped or not.
* @return array
* @throws \coding_exception|\dml_exception
*/
public static function question_category_options(array $contexts, bool $top = false, int $currentcat = 0,
bool $popupform = false, int $nochildrenof = -1,
bool $escapecontextnames = true): array {
global $CFG;
$pcontexts = [];
foreach ($contexts as $context) {
$pcontexts[] = $context->id;
}
$contextslist = join(', ', $pcontexts);
$categories = self::get_categories_for_contexts($contextslist, 'parent, sortorder, name ASC', $top);
if ($top) {
$categories = self::question_fix_top_names($categories);
}
$categories = self::question_add_context_in_key($categories);
$categories = self::add_indented_names($categories, $nochildrenof);
// Sort cats out into different contexts.
$categoriesarray = [];
foreach ($pcontexts as $contextid) {
$context = \context::instance_by_id($contextid);
$contextstring = $context->get_context_name(true, true, $escapecontextnames);
foreach ($categories as $category) {
if ($category->contextid == $contextid) {
$cid = $category->id;
if ($currentcat != $cid || $currentcat == 0) {
$a = new \stdClass;
$a->name = format_string($category->indentedname, true,
['context' => $context]);
if ($category->idnumber !== null && $category->idnumber !== '') {
$a->idnumber = s($category->idnumber);
}
if (!empty($category->questioncount)) {
$a->questioncount = $category->questioncount;
}
if (isset($a->idnumber) && isset($a->questioncount)) {
$formattedname = get_string('categorynamewithidnumberandcount', 'question', $a);
} else if (isset($a->idnumber)) {
$formattedname = get_string('categorynamewithidnumber', 'question', $a);
} else if (isset($a->questioncount)) {
$formattedname = get_string('categorynamewithcount', 'question', $a);
} else {
$formattedname = $a->name;
}
$categoriesarray[$contextstring][$cid] = $formattedname;
}
}
}
}
if ($popupform) {
$popupcats = [];
foreach ($categoriesarray as $contextstring => $optgroup) {
$group = [];
foreach ($optgroup as $key => $value) {
$key = str_replace($CFG->wwwroot, '', $key);
$group[$key] = $value;
}
$popupcats[] = [$contextstring => $group];
}
return $popupcats;
} else {
return $categoriesarray;
}
}
/**
* Add context in categories key.
*
* @param array $categories The list of categories.
* @return array
*/
public static function question_add_context_in_key(array $categories): array {
$newcatarray = [];
foreach ($categories as $id => $category) {
$category->parent = "$category->parent,$category->contextid";
$category->id = "$category->id,$category->contextid";
$newcatarray["$id,$category->contextid"] = $category;
}
return $newcatarray;
}
/**
* 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.
* @param bool $escape Whether the returned name of the thing is to be HTML escaped or not.
* @return array The same question category list given to the function, with the top category names being translated.
* @throws \coding_exception
*/
public static function question_fix_top_names(array $categories, bool $escape = true): array {
foreach ($categories as $id => $category) {
if ($category->parent == 0) {
$context = \context::instance_by_id($category->contextid);
$categories[$id]->name = get_string('topfor', 'question', $context->get_context_name(false, false, $escape));
}
}
return $categories;
}
}

View File

@ -0,0 +1,42 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace qbank_managecategories;
/**
* Class navigation.
*
* Plugin entrypoint for navigation.
*
* @package qbank_managecategories
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Guillermo Gomez Arias <guillermogomez@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class navigation extends \core_question\local\bank\navigation_node_base {
public function get_navigation_title(): string {
return get_string('categories', 'question');
}
public function get_navigation_key(): string {
return 'categories';
}
public function get_navigation_url(): \moodle_url {
return new \moodle_url('/question/bank/managecategories/category.php');
}
}

View File

@ -0,0 +1,35 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace qbank_managecategories;
/**
* Class plugin_feature.
*
* Entry point for qbank plugin.
* Every qbank plugin must extent this class.
*
* @package qbank_managecategories
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class plugin_feature extends \core_question\local\bank\plugin_features_base {
public function get_navigation_node(): ?object {
return new navigation();
}
}

View File

@ -0,0 +1,38 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace qbank_managecategories\privacy;
/**
* Privacy Subsystem for qbank_managecategories implementing null_provider.
*
* @package qbank_managecategories
* @category privacy
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Guillermo Gomez Arias <guillermogomez@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements \core_privacy\local\metadata\null_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason(): string {
return 'privacy:metadata';
}
}

View File

@ -0,0 +1,135 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace qbank_managecategories;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir. '/listlib.php');
use stdClass;
use moodle_list;
/**
* Class representing a list of question categories.
*
* @package qbank_managecategories
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class question_category_list extends moodle_list {
/**
* Table name.
* @var $table
*/
public $table = "question_categories";
/**
* List item class name.
* @var $listitemclassname
*/
public $listitemclassname = '\qbank_managecategories\question_category_list_item';
/**
* Reference to list displayed below this one.
* @var $nextlist
*/
public $nextlist = null;
/**
* Reference to list displayed above this one.
* @var $lastlist
*/
public $lastlist = null;
/**
* Context.
* @var $context
*/
public $context = null;
/**
* Sort by string.
* @var $sortby
*/
public $sortby = 'parent, sortorder, name';
/**
* Constructor.
*
* @param string $type
* @param string $attributes
* @param boolean $editable
* @param \moodle_url $pageurl url for this page
* @param integer $page if 0 no pagination. (These three params only used in top level list.)
* @param string $pageparamname name of url param that is used for passing page no
* @param integer $itemsperpage no of top level items.
* @param \context $context
*/
public function __construct($type='ul', $attributes='', $editable = false, $pageurl=null,
$page = 0, $pageparamname = 'page', $itemsperpage = 20, $context = null) {
parent::__construct('ul', '', $editable, $pageurl, $page, 'cpage', $itemsperpage);
$this->context = $context;
}
/**
* Set the array of records of list items.
*/
public function get_records() : void {
$this->records = helper::get_categories_for_contexts($this->context->id, $this->sortby);
}
/**
* Returns the highest category id that the $item can have as its parent.
* Note: question categories cannot go higher than the TOP category.
*
* @param \list_item $item The item which its top level parent is going to be returned.
* @return int
*/
public function get_top_level_parent_id($item) : int {
// Put the item at the highest level it can go.
$topcategory = question_get_top_category($item->item->contextid, true);
return $topcategory->id;
}
/**
* Process any actions.
*
* @param integer $left id of item to move left
* @param integer $right id of item to move right
* @param integer $moveup id of item to move up
* @param integer $movedown id of item to move down
* @return void
*/
public function process_actions($left, $right, $moveup, $movedown) : void {
$category = new stdClass();
if (!empty($left)) {
// Moved Left (In to another category).
$category->id = $left;
$category->contextid = $this->context->id;
$event = \core\event\question_category_moved::create_from_question_category_instance($category);
$event->trigger();
} else if (!empty($right)) {
// Moved Right (Out of the current category).
$category->id = $right;
$category->contextid = $this->context->id;
$event = \core\event\question_category_moved::create_from_question_category_instance($category);
$event->trigger();
}
parent::process_actions($left, $right, $moveup, $movedown);
}
}

View File

@ -0,0 +1,116 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace qbank_managecategories;
use moodle_url;
/**
* An item in a list of question categories.
*
* @package qbank_managecategories
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class question_category_list_item extends \list_item {
/**
* Override set_icon_html function.
*
* @param bool $first Is the first on the list.
* @param bool $last Is the last on the list.
* @param \list_item $lastitem Last item.
*/
public function set_icon_html($first, $last, $lastitem) : void {
global $CFG;
$category = $this->item;
$url = new moodle_url('/question/bank/managecategories/category.php',
($this->parentlist->pageurl->params() + ['edit' => $category->id]));
$this->icons['edit'] = $this->image_icon(get_string('editthiscategory', 'question'), $url, 'edit');
parent::set_icon_html($first, $last, $lastitem);
$toplevel = ($this->parentlist->parentitem === null);// This is a top level item.
if (($this->parentlist->nextlist !== null) && $last && $toplevel && (count($this->parentlist->items) > 1)) {
$url = new moodle_url($this->parentlist->pageurl,
[
'movedowncontext' => $this->id,
'tocontext' => $this->parentlist->nextlist->context->id,
'sesskey' => sesskey()
]
);
$this->icons['down'] = $this->image_icon(
get_string('shareincontext', 'question',
$this->parentlist->nextlist->context->get_context_name()), $url, 'down');
}
if (($this->parentlist->lastlist !== null) && $first && $toplevel && (count($this->parentlist->items) > 1)) {
$url = new moodle_url($this->parentlist->pageurl,
[
'moveupcontext' => $this->id,
'tocontext' => $this->parentlist->lastlist->context->id,
'sesskey' => sesskey()
]
);
$this->icons['up'] = $this->image_icon(
get_string('shareincontext', 'question',
$this->parentlist->lastlist->context->get_context_name()), $url, 'up');
}
}
/**
* Override item_html function.
*
* @param array $extraargs
* @return string Item html.
* @throws \moodle_exception
*/
public function item_html($extraargs = []) : string {
global $PAGE, $OUTPUT;
$str = $extraargs['str'];
$category = $this->item;
// Each section adds html to be displayed as part of this list item.
$nodeparent = $PAGE->settingsnav->find('questionbank', \navigation_node::TYPE_CONTAINER);
$questionbankurl = new moodle_url($nodeparent->action->get_path(), $this->parentlist->pageurl->params());
$questionbankurl->param('cat', $category->id . ',' . $category->contextid);
$categoryname = format_string($category->name, true, ['context' => $this->parentlist->context]);
$idnumber = null;
if ($category->idnumber !== null && $category->idnumber !== '') {
$idnumber = $category->idnumber;
}
$questioncount = ' (' . $category->questioncount . ')';
$categorydesc = format_text($category->info, $category->infoformat,
['context' => $this->parentlist->context, 'noclean' => true]);
// Don't allow delete if this is the top category, or the last editable category in this context.
$deleteurl = null;
if ($category->parent && !helper::question_is_only_child_of_top_category_in_context($category->id)) {
$deleteurl = new moodle_url($this->parentlist->pageurl, ['delete' => $this->id, 'sesskey' => sesskey()]);
}
// Render each question category.
$data =
[
'questionbankurl' => $questionbankurl,
'categoryname' => $categoryname,
'idnumber' => $idnumber,
'questioncount' => $questioncount,
'categorydesc' => $categorydesc,
'deleteurl' => $deleteurl,
'deletetitle' => $str->delete
];
return $OUTPUT->render_from_template(helper::PLUGINNAME . '/listitem', $data);
}
}

View File

@ -0,0 +1,504 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace qbank_managecategories;
/**
* QUESTION_PAGE_LENGTH - Number of categories to display on page.
*/
define('QUESTION_PAGE_LENGTH', 25);
use context;
use moodle_exception;
use moodle_url;
use qbank_managecategories\form\question_category_edit_form;
use question_bank;
use stdClass;
/**
* Class for performing operations on question categories.
*
* @package qbank_managecategories
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class question_category_object {
/**
* @var array common language strings.
*/
public $str;
/**
* @var array nested lists to display categories.
*/
public $editlists = [];
/**
* @var string tab.
*/
public $tab;
/**
* @var int tab size.
*/
public $tabsize = 3;
/**
* @var moodle_url Object representing url for this page
*/
public $pageurl;
/**
* @var question_category_edit_form Object representing form for adding / editing categories.
*/
public $catform;
/**
* Constructor.
*
* @param int $page page number.
* @param moodle_url $pageurl base URL of the display categories page. Used for redirects.
* @param context[] $contexts contexts where the current user can edit categories.
* @param int $currentcat id of the category to be edited. 0 if none.
* @param int|null $defaultcategory id of the current category. null if none.
* @param int $todelete id of the category to delete. 0 if none.
* @param context[] $addcontexts contexts where the current user can add questions.
*/
public function __construct($page, $pageurl, $contexts, $currentcat, $defaultcategory, $todelete, $addcontexts) {
$this->tab = str_repeat('&nbsp;', $this->tabsize);
$this->str = new stdClass();
$this->str->course = get_string('course');
$this->str->category = get_string('category', 'question');
$this->str->categoryinfo = get_string('categoryinfo', 'question');
$this->str->questions = get_string('questions', 'question');
$this->str->add = get_string('add');
$this->str->delete = get_string('delete');
$this->str->moveup = get_string('moveup');
$this->str->movedown = get_string('movedown');
$this->str->edit = get_string('editthiscategory', 'question');
$this->str->hide = get_string('hide');
$this->str->order = get_string('order');
$this->str->parent = get_string('parent', 'question');
$this->str->add = get_string('add');
$this->str->action = get_string('action');
$this->str->top = get_string('top');
$this->str->addcategory = get_string('addcategory', 'question');
$this->str->editcategory = get_string('editcategory', 'question');
$this->str->cancel = get_string('cancel');
$this->str->editcategories = get_string('editcategories', 'question');
$this->str->page = get_string('page');
$this->pageurl = $pageurl;
$this->initialize($page, $contexts, $currentcat, $defaultcategory, $todelete, $addcontexts);
}
/**
* Initializes this classes general category-related variables
*
* @param int $page page number.
* @param context[] $contexts contexts where the current user can edit categories.
* @param int $currentcat id of the category to be edited. 0 if none.
* @param int|null $defaultcategory id of the current category. null if none.
* @param int $todelete id of the category to delete. 0 if none.
* @param context[] $addcontexts contexts where the current user can add questions.
*/
public function initialize($page, $contexts, $currentcat, $defaultcategory, $todelete, $addcontexts): void {
$lastlist = null;
foreach ($contexts as $context) {
$this->editlists[$context->id] =
new question_category_list('ul', '', true, $this->pageurl, $page, 'cpage', QUESTION_PAGE_LENGTH, $context);
$this->editlists[$context->id]->lastlist =& $lastlist;
if ($lastlist !== null) {
$lastlist->nextlist =& $this->editlists[$context->id];
}
$lastlist =& $this->editlists[$context->id];
}
$count = 1;
$paged = false;
foreach ($this->editlists as $key => $list) {
list($paged, $count) = $this->editlists[$key]->list_from_records($paged, $count);
}
$this->catform = new question_category_edit_form($this->pageurl, compact('contexts', 'currentcat'));
if (!$currentcat) {
$this->catform->set_data(['parent' => $defaultcategory]);
}
}
/**
* Displays the user interface.
*
*/
public function display_user_interface(): void {
// Interface for editing existing categories.
$this->output_edit_lists();
echo \html_writer::empty_tag('br');
// Interface for adding a new category.
$this->output_new_table();
echo \html_writer::empty_tag('br');
}
/**
* Outputs a table to allow entry of a new category
*/
public function output_new_table(): void {
$this->catform->display();
}
/**
* Outputs a list to allow editing/rearranging of existing categories.
*
* $this->initialize() must have already been called
*
*/
public function output_edit_lists(): void {
global $OUTPUT;
echo $OUTPUT->heading_with_help(get_string('editcategories', 'question'), 'editcategories', 'question');
foreach ($this->editlists as $context => $list) {
$listhtml = $list->to_html(0, ['str' => $this->str]);
if ($listhtml) {
echo $OUTPUT->box_start('boxwidthwide boxaligncenter generalbox questioncategories contextlevel' .
$list->context->contextlevel);
$fullcontext = context::instance_by_id($context);
echo $OUTPUT->heading(get_string('questioncatsfor', 'question', $fullcontext->get_context_name()), 3);
echo $listhtml;
echo $OUTPUT->box_end();
}
}
echo $list->display_page_numbers();
}
/**
* Gets all the courseids for the given categories.
*
* @param array $categories contains category objects in a tree representation
* @return array courseids flat array in form categoryid=>courseid
*/
public function get_course_ids(array $categories): array {
$courseids = [];
foreach ($categories as $key => $cat) {
$courseids[$key] = $cat->course;
if (!empty($cat->children)) {
$courseids = array_merge($courseids, $this->get_course_ids($cat->children));
}
}
return $courseids;
}
/**
* Edit a category.
*
* @param int $categoryid Category id.
*/
public function edit_single_category(int $categoryid): void {
// Interface for adding a new category.
global $DB;
// Interface for editing existing categories.
$category = $DB->get_record("question_categories", ["id" => $categoryid]);
if (empty($category)) {
throw new moodle_exception('invalidcategory', '', '', $categoryid);
} else if ($category->parent == 0) {
throw new moodle_exception('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();
}
}
/**
* Sets the viable parents.
*
* Viable parents are any except for the category itself, or any of it's descendants
* The parentstrings parameter is passed by reference and changed by this function.
*
* @param array $parentstrings a list of parentstrings
* @param object $category Category object
*/
public function set_viable_parents(array &$parentstrings, object $category): void {
unset($parentstrings[$category->id]);
if (isset($category->children)) {
foreach ($category->children as $child) {
$this->set_viable_parents($parentstrings, $child);
}
}
}
/**
* Gets question categories.
*
* @param int|null $parent - if given, restrict records to those with this parent id.
* @param string $sort - [[sortfield [,sortfield]] {ASC|DESC}].
* @return array categories.
*/
public function get_question_categories(int $parent = null, string $sort = "sortorder ASC"): array {
global $COURSE, $DB;
if (is_null($parent)) {
$categories = $DB->get_records('question_categories', ['course' => $COURSE->id], $sort);
} else {
$select = "parent = ? AND course = ?";
$categories = $DB->get_records_select('question_categories', $select, [$parent, $COURSE->id], $sort);
}
return $categories;
}
/**
* Deletes an existing question category.
*
* @param int $categoryid id of category to delete.
*/
public function delete_category(int $categoryid): void {
global $CFG, $DB;
helper::question_can_delete_cat($categoryid);
if (!$category = $DB->get_record("question_categories", ["id" => $categoryid])) { // Security.
throw new moodle_exception('unknowcategory');
}
// Send the children categories to live with their grandparent.
$DB->set_field("question_categories", "parent", $category->parent, ["parent" => $category->id]);
// Finally delete the category itself.
$DB->delete_records("question_categories", ["id" => $category->id]);
// Log the deletion of this category.
$event = \core\event\question_category_deleted::create_from_question_category_instance($category);
$event->add_record_snapshot('question_categories', $category);
$event->trigger();
}
/**
* Move questions and then delete the category.
*
* @param int $oldcat id of the old category.
* @param int $newcat id of the new category.
*/
public function move_questions_and_delete_category(int $oldcat, int $newcat): void {
helper::question_can_delete_cat($oldcat);
$this->move_questions($oldcat, $newcat);
$this->delete_category($oldcat);
}
/**
* Display the form to move a category.
*
* @param int $questionsincategory
* @param object $category
* @throws \coding_exception
*/
public function display_move_form($questionsincategory, $category): void {
global $OUTPUT;
$vars = new stdClass();
$vars->name = $category->name;
$vars->count = $questionsincategory;
echo $OUTPUT->box(get_string('categorymove', 'question', $vars), 'generalbox boxaligncenter');
$this->moveform->display();
}
/**
* Move questions to another category.
*
* @param int $oldcat id of the old category.
* @param int $newcat id of the new category.
* @throws \dml_exception
*/
public function move_questions(int $oldcat, int $newcat): void {
global $DB;
$questionids = $DB->get_records_select_menu('question',
'category = ? AND (parent = 0 OR parent = id)', [$oldcat], '', 'id,1');
question_move_questions_to_category(array_keys($questionids), $newcat);
}
/**
* Create a new category.
*
* Data is expected to come from question_category_edit_form.
*
* By default redirects on success, unless $return is true.
*
* @param string $newparent 'categoryid,contextid' of the parent category.
* @param string $newcategory the name.
* @param string $newinfo the description.
* @param bool $return if true, return rather than redirecting.
* @param int|string $newinfoformat description format. One of the FORMAT_ constants.
* @param null $idnumber the idnumber. '' is converted to null.
* @return bool|int New category id if successful, else false.
*/
public function add_category($newparent, $newcategory, $newinfo, $return = false, $newinfoformat = FORMAT_HTML,
$idnumber = null): int {
global $DB;
if (empty($newcategory)) {
throw new moodle_exception('categorynamecantbeblank', 'question');
}
list($parentid, $contextid) = explode(',', $newparent);
// ...moodle_form makes sure select element output is legal no need for further cleaning.
require_capability('moodle/question:managecategory', context::instance_by_id($contextid));
if ($parentid) {
if (!($DB->get_field('question_categories', 'contextid', ['id' => $parentid]) == $contextid)) {
throw new moodle_exception('cannotinsertquestioncatecontext', 'question', '',
['cat' => $newcategory, 'ctx' => $contextid]);
}
}
if ((string) $idnumber === '') {
$idnumber = null;
} else if (!empty($contextid)) {
// While this check already exists in the form validation, this is a backstop preventing unnecessary errors.
if ($DB->record_exists('question_categories',
['idnumber' => $idnumber, 'contextid' => $contextid])) {
$idnumber = null;
}
}
$cat = new stdClass();
$cat->parent = $parentid;
$cat->contextid = $contextid;
$cat->name = $newcategory;
$cat->info = $newinfo;
$cat->infoformat = $newinfoformat;
$cat->sortorder = 999;
$cat->stamp = make_unique_id_code();
$cat->idnumber = $idnumber;
$categoryid = $DB->insert_record("question_categories", $cat);
// Log the creation of this category.
$category = new stdClass();
$category->id = $categoryid;
$category->contextid = $contextid;
$event = \core\event\question_category_created::create_from_question_category_instance($category);
$event->trigger();
if ($return) {
return $categoryid;
} else {
// Always redirect after successful action.
redirect($this->pageurl);
}
}
/**
* Updates an existing category with given params.
*
* Warning! parameter order and meaning confusingly different from add_category in some ways!
*
* @param int $updateid id of the category to update.
* @param int $newparent 'categoryid,contextid' of the parent category to set.
* @param string $newname category name.
* @param string $newinfo category description.
* @param int|string $newinfoformat description format. One of the FORMAT_ constants.
* @param int $idnumber the idnumber. '' is converted to null.
* @param bool $redirect if true, will redirect once the DB is updated (default).
*/
public function update_category($updateid, $newparent, $newname, $newinfo, $newinfoformat = FORMAT_HTML,
$idnumber = null, $redirect = true): void {
global $CFG, $DB;
if (empty($newname)) {
throw new moodle_exception('categorynamecantbeblank', 'question');
}
// Get the record we are updating.
$oldcat = $DB->get_record('question_categories', ['id' => $updateid]);
$lastcategoryinthiscontext = helper::question_is_only_child_of_top_category_in_context($updateid);
if (!empty($newparent) && !$lastcategoryinthiscontext) {
list($parentid, $tocontextid) = explode(',', $newparent);
} else {
$parentid = $oldcat->parent;
$tocontextid = $oldcat->contextid;
}
// Check permissions.
$fromcontext = context::instance_by_id($oldcat->contextid);
require_capability('moodle/question:managecategory', $fromcontext);
// If moving to another context, check permissions some more, and confirm contextid,stamp uniqueness.
$newstamprequired = false;
if ($oldcat->contextid != $tocontextid) {
$tocontext = context::instance_by_id($tocontextid);
require_capability('moodle/question:managecategory', $tocontext);
// Confirm stamp uniqueness in the new context. If the stamp already exists, generate a new one.
if ($DB->record_exists('question_categories', ['contextid' => $tocontextid, 'stamp' => $oldcat->stamp])) {
$newstamprequired = true;
}
}
if ((string) $idnumber === '') {
$idnumber = null;
} else if (!empty($tocontextid)) {
// While this check already exists in the form validation, this is a backstop preventing unnecessary errors.
if ($DB->record_exists_select('question_categories',
'idnumber = ? AND contextid = ? AND id <> ?',
[$idnumber, $tocontextid, $updateid])) {
$idnumber = null;
}
}
// Update the category record.
$cat = new stdClass();
$cat->id = $updateid;
$cat->name = $newname;
$cat->info = $newinfo;
$cat->infoformat = $newinfoformat;
$cat->parent = $parentid;
$cat->contextid = $tocontextid;
$cat->idnumber = $idnumber;
if ($newstamprequired) {
$cat->stamp = make_unique_id_code();
}
$DB->update_record('question_categories', $cat);
// Log the update of this category.
$event = \core\event\question_category_updated::create_from_question_category_instance($cat);
$event->trigger();
// If the category name has changed, rename any random questions in that category.
if ($oldcat->name != $cat->name) {
$where = "qtype = 'random' AND category = ? AND " . $DB->sql_compare_text('questiontext') . " = ?";
$randomqtype = question_bank::get_qtype('random');
$randomqname = $randomqtype->question_name($cat, false);
$DB->set_field_select('question', 'name', $randomqname, $where, [$cat->id, '0']);
$randomqname = $randomqtype->question_name($cat, true);
$DB->set_field_select('question', 'name', $randomqname, $where, [$cat->id, '1']);
}
if ($oldcat->contextid != $tocontextid) {
// Moving to a new context. Must move files belonging to questions.
question_move_category_to_context($cat->id, $oldcat->contextid, $tocontextid);
}
// Cat param depends on the context id, so update it.
$this->pageurl->param('cat', $updateid . ',' . $tocontextid);
if ($redirect) {
// Always redirect after successful action.
redirect($this->pageurl);
}
}
}

View File

@ -0,0 +1,27 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Strings for component 'qbank_managecategories', language 'en'
*
* @package qbank_managecategories
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Guillermo Gomez Arias <guillermogomez@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['pluginname'] = 'Manage categories';
$string['privacy:metadata'] = 'The Manage Categories plugin does not store any user data.';

View File

@ -0,0 +1,57 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template qbank_managecategories/listitem
This template renders the list item for each category.
Example context (json):
{
"questionbankurl": "question/edit.php?cmid=",
"categoryname": "Default for Miscellaneous",
"idnumber": "1",
"questioncount": "1",
"categorydesc": "The default category for questions shared in context Miscellaneous",
"deleteurl": "url",
"deletetitle": "Advanced"
}
}}
<b>
<a title="{{#str}}editquestions, question{{/str}}" href="{{{questionbankurl}}}">
{{categoryname}}
{{#idnumber}}
<span class="badge badge-primary">
<span class="accesshide">
{{#str}}idnumber, question{{/str}}
</span>
{{idnumber}}
</span>
{{/idnumber}}
{{questioncount}}
</a>
</b>
{{#categorydesc}}
<div class="text_to_html">
{{{categorydesc}}}
</div>
{{/categorydesc}}
{{#deleteurl}}
<a title="{{deletetitle}}" href="{{{deleteurl}}}">
{{# pix }} t/delete, core, {{deletetitle}} {{/ pix }}
</a>
{{/deleteurl}}

View File

@ -0,0 +1,66 @@
@qbank @qbank_managecategories @javascript
Feature: A teacher can put questions in categories in the question bank
In order to organize my questions
As a teacher
I create and edit categories and move questions between them
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | format |
| Course 1 | C1 | weeks |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "question categories" exist:
| contextlevel | reference | questioncategory | name |
| Course | C1 | Top | top |
| Course | C1 | top | Default for C1 |
| Course | C1 | Default for C1 | Subcategory |
| 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 |
And I log in as "teacher1"
And I am on "Course 1" course homepage
Scenario: A new question category can be created
When I navigate to "Question bank > Categories" in current page administration
And I set the following fields to these values:
| Name | New Category 1 |
| Parent category | Top |
| Category info | Created as a test |
| ID number | newcatidnumber |
And I press "submitbutton"
Then I should see "New Category 1"
And I should see "ID number"
And I should see "newcatidnumber"
And I should see "(0)"
And I should see "Created as a test" in the "New Category 1" "list_item"
And "New Category 1 [newcatidnumber]" "option" should exist in the "Parent category" "select"
Scenario: A question category can be edited
When I navigate to "Question bank > Categories" in current page administration
And I click on "Edit this category" "link" in the "Subcategory" "list_item"
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 |
And I press "Save changes"
Then I should see "New name"
And I should see "I was edited" in the "New name" "list_item"
Scenario: An empty question category can be deleted
When I navigate to "Question bank > Categories" in current page administration
And I click on "Delete" "link" in the "Subcategory" "list_item"
Then I should not see "Subcategory"
Scenario: An non-empty question category can be deleted if you move the contents elsewhere
When I navigate to "Question bank > Categories" in current page administration
And I click on "Delete" "link" in the "Used category" "list_item"
And I should see "The category 'Used category' contains 1 questions"
And I press "Save in category"
Then I should not see "Used category"
And I should see "Default for C1 (1)"

View File

@ -0,0 +1,52 @@
@qbank @qbank_managecategories
Feature: A teacher can put questions with idnumbers in categories with idnumbers in the question bank
In order to organize my questions
As a teacher
I create and edit categories (now with idnumbers)
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | format |
| Course 1 | C1 | weeks |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And I log in as "teacher1"
And I am on "Course 1" course homepage
Scenario: A new question category can only be created with a unique idnumber for a context
# Note need to create the top category each time.
When the following "question categories" exist:
| contextlevel | reference | questioncategory | name | idnumber |
| Course | C1 | Top | top | |
| Course | C1 | top | Used category | c1used |
And I navigate to "Question bank > Categories" in current page administration
And I set the following fields to these values:
| Name | Sub used category |
| Parent category | Used category |
| Category info | Created as a test |
| ID number | c1used |
And I press "Add category"
# Standard warning.
Then I should see "This ID number is already in use"
# Correction to a unique idnumber for the context.
And I set the field "ID number" to "c1unused"
And I press "Add category"
Then I should see "Sub used category"
And I should see "ID number"
And I should see "c1unused"
And I should see "(0)"
And I should see "Created as a test" in the "Sub used category" "list_item"
Scenario: A question category can be edited and saved without changing the idnumber
When the following "question categories" exist:
| contextlevel | reference | questioncategory | name | idnumber |
| Course | C1 | Top | top | |
| Course | C1 | top | Used category | c1used |
And I navigate to "Question bank > Categories" in current page administration
And I click on "Edit this category" "link" in the "Used category" "list_item"
And I press "Save changes"
Then I should not see "This ID number is already in use"

View File

@ -0,0 +1,50 @@
@qbank @qbank_managecategories @javascript
Feature: Use the qbank plugin manager page for managecategories
In order to check the plugin behaviour with enable and disable
Background:
Given the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "activities" exist:
| activity | name | course | idnumber |
| quiz | Test quiz | C1 | quiz1 |
And the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Test questions |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | First question | Answer the first question |
Scenario: Enable/disable managecategories plugin from the base view
Given I log in as "admin"
When I navigate to "Plugins > Question bank plugins > Manage question bank plugins" in site administration
And I should see "Manage categories"
And I click on "Disable" "link" in the "Manage categories" "table_row"
And I am on the "Test quiz" "quiz activity" page
And I navigate to "Question bank" in current page administration
Then I should not see "Categories"
And I navigate to "Plugins > Question bank plugins > Manage question bank plugins" in site administration
And I click on "Enable" "link" in the "Manage categories" "table_row"
And I am on the "Test quiz" "quiz activity" page
And I navigate to "Question bank" in current page administration
And I should see "Categories"
Scenario: Enable/disable the tab New category when tyring to add a random question to a quiz
Given I log in as "admin"
When I navigate to "Plugins > Question bank plugins > Manage question bank plugins" in site administration
And I should see "Manage categories"
And I click on "Disable" "link" in the "Manage categories" "table_row"
And I am on the "Test quiz" "quiz activity" page
And I navigate to "Edit quiz" in current page administration
And I open the "last" add to quiz menu
And I follow "a random question"
Then I should not see "New category"
And I press "id_cancel"
And I navigate to "Plugins > Question bank plugins > Manage question bank plugins" in site administration
And I click on "Enable" "link" in the "Manage categories" "table_row"
And I am on the "Test quiz" "quiz activity" page
And I navigate to "Edit quiz" in current page administration
And I open the "last" add to quiz menu
And I follow "a random question"
And I should see "New category"

View File

@ -0,0 +1,228 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace qbank_managecategories;
/**
* Unit tests for helper class.
*
* @package qbank_managecategories
* @copyright 2006 The Open University
* @author 2021, Guillermo Gomez Arias <guillermogomez@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \qbank_managecategories\helper
*/
class helper_test extends \advanced_testcase {
/**
* @var \context_module module context.
*/
protected $context;
/**
* @var \stdClass course object.
*/
protected $course;
/**
* @var \component_generator_base question generator.
*/
protected $qgenerator;
/**
* @var \stdClass quiz object.
*/
protected $quiz;
/**
* Tests initial setup.
*/
protected function setUp(): void {
parent::setUp();
self::setAdminUser();
$this->resetAfterTest();
$datagenerator = $this->getDataGenerator();
$this->course = $datagenerator->create_course();
$this->quiz = $datagenerator->create_module('quiz', ['course' => $this->course->id]);
$this->qgenerator = $datagenerator->get_plugin_generator('core_question');
$this->context = \context_module::instance($this->quiz->cmid);
}
/**
* Test question_remove_stale_questions_from_category function.
*
* @covers ::question_remove_stale_questions_from_category
*/
public function test_question_remove_stale_questions_from_category() {
global $DB;
$qcat1 = $this->qgenerator->create_question_category(['contextid' => $this->context->id]);
$q1a = $this->qgenerator->create_question('shortanswer', null, ['category' => $qcat1->id]); // Will be hidden.
$DB->set_field('question', 'hidden', 1, ['id' => $q1a->id]);
$qcat2 = $this->qgenerator->create_question_category(['contextid' => $this->context->id]);
$q2a = $this->qgenerator->create_question('shortanswer', null, ['category' => $qcat2->id]); // Will be hidden.
$q2b = $this->qgenerator->create_question('shortanswer', null, ['category' => $qcat2->id]); // Will be hidden but used.
$DB->set_field('question', 'hidden', 1, ['id' => $q2a->id]);
$DB->set_field('question', 'hidden', 1, ['id' => $q2b->id]);
quiz_add_quiz_question($q2b->id, $this->quiz);
quiz_add_random_questions($this->quiz, 0, $qcat2->id, 1, false);
// We added one random question to the quiz and we expect the quiz to have only one random question.
$q2d = $DB->get_record_sql("SELECT q.*
FROM {question} q
JOIN {quiz_slots} s ON s.questionid = q.id
WHERE q.qtype = :qtype
AND s.quizid = :quizid",
['qtype' => 'random', 'quizid' => $this->quiz->id], MUST_EXIST);
// The following 2 lines have to be after the quiz_add_random_questions() call above.
// Otherwise, quiz_add_random_questions() will to be "smart" and use them instead of creating a new "random" question.
$q1b = $this->qgenerator->create_question('random', null, ['category' => $qcat1->id]); // Will not be used.
$q2c = $this->qgenerator->create_question('random', null, ['category' => $qcat2->id]); // Will not be used.
$this->assertEquals(2, $DB->count_records('question', ['category' => $qcat1->id]));
$this->assertEquals(4, $DB->count_records('question', ['category' => $qcat2->id]));
// Non-existing category, nothing will happen.
helper::question_remove_stale_questions_from_category(0);
$this->assertEquals(2, $DB->count_records('question', ['category' => $qcat1->id]));
$this->assertEquals(4, $DB->count_records('question', ['category' => $qcat2->id]));
// First category, should be empty afterwards.
helper::question_remove_stale_questions_from_category($qcat1->id);
$this->assertEquals(0, $DB->count_records('question', ['category' => $qcat1->id]));
$this->assertEquals(4, $DB->count_records('question', ['category' => $qcat2->id]));
$this->assertFalse($DB->record_exists('question', ['id' => $q1a->id]));
$this->assertFalse($DB->record_exists('question', ['id' => $q1b->id]));
// Second category, used questions should be left untouched.
helper::question_remove_stale_questions_from_category($qcat2->id);
$this->assertEquals(0, $DB->count_records('question', ['category' => $qcat1->id]));
$this->assertEquals(2, $DB->count_records('question', ['category' => $qcat2->id]));
$this->assertFalse($DB->record_exists('question', ['id' => $q2a->id]));
$this->assertTrue($DB->record_exists('question', ['id' => $q2b->id]));
$this->assertFalse($DB->record_exists('question', ['id' => $q2c->id]));
$this->assertTrue($DB->record_exists('question', ['id' => $q2d->id]));
}
/**
* Test delete top category in function question_can_delete_cat.
*
* @covers ::question_can_delete_cat
* @covers ::question_is_top_category
*/
public function test_question_can_delete_cat_top_category() {
$qcategory1 = $this->qgenerator->create_question_category(['contextid' => $this->context->id]);
// Try to delete a top category.
$categorytop = question_get_top_category($qcategory1->id, true)->id;
$this->expectException('moodle_exception');
$this->expectExceptionMessage(get_string('cannotdeletetopcat', 'question'));
helper::question_can_delete_cat($categorytop);
}
/**
* Test delete only child category in function question_can_delete_cat.
*
* @covers ::question_can_delete_cat
* @covers ::question_is_only_child_of_top_category_in_context
*/
public function test_question_can_delete_cat_child_category() {
$qcategory1 = $this->qgenerator->create_question_category(['contextid' => $this->context->id]);
// Try to delete an only child of top category having also at least one child.
$this->expectException('moodle_exception');
$this->expectExceptionMessage(get_string('cannotdeletecate', 'question'));
helper::question_can_delete_cat($qcategory1->id);
}
/**
* Test delete category in function question_can_delete_cat without capabilities.
*
* @covers ::question_can_delete_cat
*/
public function test_question_can_delete_cat_capability() {
$qcategory1 = $this->qgenerator->create_question_category(['contextid' => $this->context->id]);
$qcategory2 = $this->qgenerator->create_question_category(['contextid' => $this->context->id, 'parent' => $qcategory1->id]);
// This call should not throw an exception as admin user has the capabilities moodle/question:managecategory.
helper::question_can_delete_cat($qcategory2->id);
// Try to delete a category with and user without the capability.
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
$this->expectException(\required_capability_exception::class);
$this->expectExceptionMessage(get_string('nopermissions', 'error', get_string('question:managecategory', 'role')));
helper::question_can_delete_cat($qcategory2->id);
}
/**
* Test question_category_select_menu function.
*
* @covers ::question_category_select_menu
* @covers ::question_category_options
*/
public function test_question_category_select_menu() {
$this->qgenerator->create_question_category(['contextid' => $this->context->id, 'name' => 'Test this question category']);
$contexts = new \question_edit_contexts($this->context);
ob_start();
helper::question_category_select_menu($contexts->having_cap('moodle/question:add'));
$output = ob_get_clean();
// Test the select menu of question categories output.
$this->assertStringContainsString('Question category', $output);
$this->assertStringContainsString('<option selected="selected" value="">choosedots</option>', $output);
$this->assertStringContainsString('Test this question category', $output);
}
/**
* Test that question_category_options function returns the correct category tree.
*
* @covers ::question_category_options
* @covers ::get_categories_for_contexts
* @covers ::question_fix_top_names
* @covers ::question_add_context_in_key
* @covers ::add_indented_names
*/
public function test_question_category_options() {
$qcategory1 = $this->qgenerator->create_question_category(['contextid' => $this->context->id]);
$qcategory2 = $this->qgenerator->create_question_category(['contextid' => $this->context->id, 'parent' => $qcategory1->id]);
$qcategory3 = $this->qgenerator->create_question_category(['contextid' => $this->context->id]);
$contexts = new \question_edit_contexts($this->context);
// Validate that we have the array with the categories tree.
$categorycontexts = helper::question_category_options($contexts->having_cap('moodle/question:add'));
foreach ($categorycontexts as $categorycontext) {
$this->assertCount(3, $categorycontext);
}
// Validate that we have the array with the categories tree and that top category is there.
$categorycontexts = helper::question_category_options($contexts->having_cap('moodle/question:add'), true);
foreach ($categorycontexts as $categorycontext) {
$this->assertCount(4, $categorycontext);
}
}
}

View File

@ -0,0 +1,343 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace qbank_managecategories;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/question/editlib.php');
use context;
use context_course;
use context_module;
use moodle_url;
use question_edit_contexts;
use stdClass;
/**
* Unit tests for qbank_managecategories\question_category_object.
*
* @package qbank_managecategories
* @copyright 2019 the Open University
* @author 2021, Guillermo Gomez Arias <guillermogomez@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \qbank_managecategories\question_category_object
*/
class question_category_object_test extends \advanced_testcase {
/**
* @var question_category_object used in the tests.
*/
protected $qcobject;
/**
* @var context a context to use.
*/
protected $context;
/**
* @var stdClass top category in context.
*/
protected $topcat;
/**
* @var stdClass course object.
*/
protected $course;
/**
* @var stdClass quiz object.
*/
protected $quiz;
/**
* @var question_edit_contexts
*/
private $qcontexts;
/**
* @var false|object|stdClass|null
*/
private $defaultcategoryobj;
/**
* @var string
*/
private $defaultcategory;
/**
* @var question_category_object
*/
private $qcobjectquiz;
protected function setUp(): void {
parent::setUp();
self::setAdminUser();
$this->resetAfterTest();
$this->context = context_course::instance(SITEID);
$contexts = new question_edit_contexts($this->context);
$this->topcat = question_get_top_category($this->context->id, true);
$this->qcobject = new question_category_object(null,
new moodle_url('/question/bank/managecategories/category.php', ['courseid' => SITEID]),
$contexts->having_one_edit_tab_cap('categories'), 0, null, 0,
$contexts->having_cap('moodle/question:add'));
// Set up tests in a quiz context.
$this->course = $this->getDataGenerator()->create_course();
$this->quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $this->course->id]);
$this->qcontexts = new question_edit_contexts(context_module::instance($this->quiz->cmid));
$this->defaultcategoryobj = question_make_default_categories([$this->qcontexts->lowest()]);
$this->defaultcategory = $this->defaultcategoryobj->id . ',' . $this->defaultcategoryobj->contextid;
$this->qcobjectquiz = new question_category_object(
1,
new moodle_url('/mod/quiz/edit.php', ['cmid' => $this->quiz->cmid]),
$this->qcontexts->having_one_edit_tab_cap('categories'),
$this->defaultcategoryobj->id,
$this->defaultcategory,
null,
$this->qcontexts->having_cap('moodle/question:add'));
}
/**
* Test creating a category.
*
* @covers ::add_category
*/
public function test_add_category_no_idnumber() {
global $DB;
$id = $this->qcobject->add_category($this->topcat->id . ',' . $this->topcat->contextid,
'New category', '', true, FORMAT_HTML, ''); // No idnumber passed as '' to match form data.
$newcat = $DB->get_record('question_categories', ['id' => $id], '*', MUST_EXIST);
$this->assertSame('New category', $newcat->name);
$this->assertNull($newcat->idnumber);
}
/**
* Test creating a category with a tricky idnumber.
*
* @covers ::add_category
*/
public function test_add_category_set_idnumber_0() {
global $DB;
$id = $this->qcobject->add_category($this->topcat->id . ',' . $this->topcat->contextid,
'New category', '', true, FORMAT_HTML, '0');
$newcat = $DB->get_record('question_categories', ['id' => $id], '*', MUST_EXIST);
$this->assertSame('New category', $newcat->name);
$this->assertSame('0', $newcat->idnumber);
}
/**
* Trying to add a category with duplicate idnumber blanks it.
* (In reality, this would probably get caught by form validation.)
*
* @covers ::add_category
*/
public function test_add_category_try_to_set_duplicate_idnumber() {
global $DB;
$this->qcobject->add_category($this->topcat->id . ',' . $this->topcat->contextid,
'Existing category', '', true, FORMAT_HTML, 'frog');
$id = $this->qcobject->add_category($this->topcat->id . ',' . $this->topcat->contextid,
'New category', '', true, FORMAT_HTML, 'frog');
$newcat = $DB->get_record('question_categories', ['id' => $id], '*', MUST_EXIST);
$this->assertSame('New category', $newcat->name);
$this->assertNull($newcat->idnumber);
}
/**
* Test updating a category.
*
* @covers ::update_category
*/
public function test_update_category() {
global $DB;
$id = $this->qcobject->add_category($this->topcat->id . ',' . $this->topcat->contextid,
'Old name', 'Description', true, FORMAT_HTML, 'frog');
$this->qcobject->update_category($id, $this->topcat->id . ',' . $this->topcat->contextid,
'New name', 'New description', FORMAT_HTML, '0', false);
$newcat = $DB->get_record('question_categories', ['id' => $id], '*', MUST_EXIST);
$this->assertSame('New name', $newcat->name);
$this->assertSame('0', $newcat->idnumber);
}
/**
* Test updating a category to remove the idnumber.
*
* @covers ::update_category
*/
public function test_update_category_removing_idnumber() {
global $DB;
$id = $this->qcobject->add_category($this->topcat->id . ',' . $this->topcat->contextid,
'Old name', 'Description', true, FORMAT_HTML, 'frog');
$this->qcobject->update_category($id, $this->topcat->id . ',' . $this->topcat->contextid,
'New name', 'New description', FORMAT_HTML, '', false);
$newcat = $DB->get_record('question_categories', ['id' => $id], '*', MUST_EXIST);
$this->assertSame('New name', $newcat->name);
$this->assertNull($newcat->idnumber);
}
/**
* Test updating a category without changing the idnumber.
*
* @covers ::update_category
*/
public function test_update_category_dont_change_idnumber() {
global $DB;
$id = $this->qcobject->add_category($this->topcat->id . ',' . $this->topcat->contextid,
'Old name', 'Description', true, FORMAT_HTML, 'frog');
$this->qcobject->update_category($id, $this->topcat->id . ',' . $this->topcat->contextid,
'New name', 'New description', FORMAT_HTML, 'frog', false);
$newcat = $DB->get_record('question_categories', ['id' => $id], '*', MUST_EXIST);
$this->assertSame('New name', $newcat->name);
$this->assertSame('frog', $newcat->idnumber);
}
/**
* Trying to update a category so its idnumber duplicates idnumber blanks it.
* (In reality, this would probably get caught by form validation.)
*
* @covers ::update_category
*/
public function test_update_category_try_to_set_duplicate_idnumber() {
global $DB;
$this->qcobject->add_category($this->topcat->id . ',' . $this->topcat->contextid,
'Existing category', '', true, FORMAT_HTML, 'toad');
$id = $this->qcobject->add_category($this->topcat->id . ',' . $this->topcat->contextid,
'old name', '', true, FORMAT_HTML, 'frog');
$this->qcobject->update_category($id, $this->topcat->id . ',' . $this->topcat->contextid,
'New name', '', FORMAT_HTML, 'toad', false);
$newcat = $DB->get_record('question_categories', ['id' => $id], '*', MUST_EXIST);
$this->assertSame('New name', $newcat->name);
$this->assertNull($newcat->idnumber);
}
/**
* Test the question category created event.
*
* @covers ::add_category
*/
public function test_question_category_created() {
// Trigger and capture the event.
$sink = $this->redirectEvents();
$categoryid = $this->qcobjectquiz->add_category($this->defaultcategory, 'newcategory', '', true);
$events = $sink->get_events();
$event = reset($events);
// Check that the event data is valid.
$this->assertInstanceOf('\core\event\question_category_created', $event);
$this->assertEquals(context_module::instance($this->quiz->cmid), $event->get_context());
$expected = [$this->course->id, 'quiz', 'addcategory', 'view.php?id=' . $this->quiz->cmid , $categoryid, $this->quiz->cmid];
$this->assertEventLegacyLogData($expected, $event);
$this->assertEventContextNotUsed($event);
}
/**
* Test the question category deleted event.
*
* @covers ::delete_category
*/
public function test_question_category_deleted() {
// Create the category.
$categoryid = $this->qcobjectquiz->add_category($this->defaultcategory, 'newcategory', '', true);
// Trigger and capture the event.
$sink = $this->redirectEvents();
$this->qcobjectquiz->delete_category($categoryid);
$events = $sink->get_events();
$event = reset($events);
// Check that the event data is valid.
$this->assertInstanceOf('\core\event\question_category_deleted', $event);
$this->assertEquals(context_module::instance($this->quiz->cmid), $event->get_context());
$this->assertEquals($categoryid, $event->objectid);
$this->assertDebuggingNotCalled();
}
/**
* Test the question category updated event.
*
* @covers ::update_category
*/
public function test_question_category_updated() {
// Create the category.
$categoryid = $this->qcobjectquiz->add_category($this->defaultcategory, 'newcategory', '', true);
// Trigger and capture the event.
$sink = $this->redirectEvents();
$this->qcobjectquiz->update_category($categoryid, $this->defaultcategory, 'updatedcategory', '', FORMAT_HTML, '', false);
$events = $sink->get_events();
$event = reset($events);
// Check that the event data is valid.
$this->assertInstanceOf('\core\event\question_category_updated', $event);
$this->assertEquals(context_module::instance($this->quiz->cmid), $event->get_context());
$this->assertEquals($categoryid, $event->objectid);
$this->assertDebuggingNotCalled();
}
/**
* Test the question category viewed event.
* There is no external API for viewing the category, so the unit test will simply
* create and trigger the event and ensure data is returned as expected.
*
* @covers ::add_category
*/
public function test_question_category_viewed() {
// Create the category.
$categoryid = $this->qcobjectquiz->add_category($this->defaultcategory, 'newcategory', '', true);
// Log the view of this category.
$category = new stdClass();
$category->id = $categoryid;
$context = context_module::instance($this->quiz->cmid);
$event = \core\event\question_category_viewed::create_from_question_category_instance($category, $context);
// Trigger and capture the event.
$sink = $this->redirectEvents();
$event->trigger();
$events = $sink->get_events();
$event = reset($events);
// Check that the event data is valid.
$this->assertInstanceOf('\core\event\question_category_viewed', $event);
$this->assertEquals(context_module::instance($this->quiz->cmid), $event->get_context());
$this->assertEquals($categoryid, $event->objectid);
$this->assertDebuggingNotCalled();
}
}

View File

@ -0,0 +1,31 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <https://www.gnu.org/licenses/>.
/**
* Plugin version and other meta-data are defined here.
*
* @package qbank_managecategories
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Guillermo Gomez Arias <guillermogomez@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'qbank_managecategories';
$plugin->version = 2021070700;
$plugin->requires = 2021052500;
$plugin->maturity = MATURITY_STABLE;

View File

@ -39,6 +39,9 @@ require_once($CFG->dirroot . '/question/move_form.php');
*
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @deprecated since Moodle 4.0 MDL-71585
* @see \qbank_managecategories\question_category_list
* @todo deprecation on MDL-71679
*/
class question_category_list extends moodle_list {
public $table = "question_categories";
@ -56,6 +59,9 @@ class question_category_list extends moodle_list {
public $sortby = 'parent, sortorder, name';
public function __construct($type='ul', $attributes='', $editable = false, $pageurl=null, $page = 0, $pageparamname = 'page', $itemsperpage = 20, $context = null){
debugging('Class question_category_list in \core_question\category_class is deprecated,
please use qbank_managecategories\question_category_list instead.', DEBUG_DEVELOPER);
parent::__construct('ul', '', $editable, $pageurl, $page, 'cpage', $itemsperpage);
$this->context = $context;
}
@ -111,9 +117,14 @@ class question_category_list extends moodle_list {
*
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @deprecated since Moodle 4.0 MDL-71585
* @see \qbank_managecategories\question_category_list_item
* @todo deprecation on MDL-71679
*/
class question_category_list_item extends list_item {
public function set_icon_html($first, $last, $lastitem){
debugging('Function set_icon_html() in \core_question\category_class is deprecated,
please use qbank_managecategories\question_category_list_item::set_icon_html() instead.', DEBUG_DEVELOPER);
global $CFG;
$category = $this->item;
$url = new moodle_url('/question/category.php', ($this->parentlist->pageurl->params() + array('edit'=>$category->id)));
@ -134,6 +145,8 @@ class question_category_list_item extends list_item {
public function item_html($extraargs = array()){
global $CFG, $PAGE, $OUTPUT;
debugging('Function item_html() in \core_question\category_class is deprecated,
please use qbank_managecategories\question_category_list_item::item_html() instead.', DEBUG_DEVELOPER);
$str = $extraargs['str'];
$category = $this->item;
@ -174,6 +187,9 @@ class question_category_list_item extends list_item {
*
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @deprecated since Moodle 4.0 MDL-71585
* @see \qbank_managecategories\question_category_object
* @todo deprecation on MDL-71679
*/
class question_category_object {
@ -211,6 +227,8 @@ class question_category_object {
* @param context[] $addcontexts contexts where the current user can add questions.
*/
public function __construct($page, $pageurl, $contexts, $currentcat, $defaultcategory, $todelete, $addcontexts) {
debugging('Class question_category_list in \core_question\category_class is deprecated,
please use qbank_managecategories\question_category_object instead.', DEBUG_DEVELOPER);
$this->tab = str_repeat('&nbsp;', $this->tabsize);

View File

@ -34,10 +34,15 @@ require_once($CFG->libdir.'/formslib.php');
*
* @copyright 2007 Jamie Pratt me@jamiep.org
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @deprecated since Moodle 4.0 MDL-71585
* @see qbank_managecategories\form\category_form
*/
class question_category_edit_form extends moodleform {
protected function definition() {
debugging('Class question_export_form in \core_question\category_form is deprecated,
please use core_question\bank\managecategories\form\category_form instead.', DEBUG_DEVELOPER);
$mform = $this->_form;
$contexts = $this->_customdata['contexts'];

View File

@ -25,6 +25,8 @@
namespace core_question\bank\search;
use qbank_managecategories\helper;
/**
* This class controls from which category questions are listed.
*
@ -116,7 +118,7 @@ class category_condition extends condition {
public function display_options() {
global $PAGE;
$displaydata = [];
$catmenu = question_category_options($this->contexts, true, 0,
$catmenu = helper::question_category_options($this->contexts, true, 0,
true, -1, false);
$displaydata['categoryselect'] = \html_writer::select($catmenu, 'category', $this->cat, [],
array('class' => 'searchoptions custom-select', 'id' => 'id_selectacategory'));

View File

@ -26,6 +26,7 @@ namespace core_question\local\bank;
use core_question\bank\search\condition;
use qbank_editquestion\editquestion_helper;
use qbank_managecategories\helper;
/**
* This class prints a view of the question bank.
@ -1057,7 +1058,7 @@ class view {
'data-toggle' => 'action',
'disabled' => true,
]);
question_category_select_menu($addcontexts, false, 0, "{$category->id},{$category->contextid}");
helper::question_category_select_menu($addcontexts, false, 0, "{$category->id},{$category->contextid}");
}
}

View File

@ -99,15 +99,16 @@ function get_questions_category( $category, $noparent=false, $recurse=true, $exp
*
* @param int $categoryid a category id.
* @return bool
* @deprecated since Moodle 4.0 MDL-71585
* @see qbank_managecategories\helper
* @todo Final deprecation on Moodle 4.4 MDL-72438
*/
function question_is_only_child_of_top_category_in_context($categoryid) {
global $DB;
return 1 == $DB->count_records_sql("
SELECT count(*)
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));
debugging('Function question_is_only_child_of_top_category_in_context()
has been deprecated and moved to qbank_managecategories plugin,
Please use qbank_managecategories\helper::question_is_only_child_of_top_category_in_context() instead.',
DEBUG_DEVELOPER);
return \qbank_managecategories\helper::question_is_only_child_of_top_category_in_contextt($categoryid);
}
/**
@ -115,27 +116,28 @@ function question_is_only_child_of_top_category_in_context($categoryid) {
*
* @param int $categoryid a category id.
* @return bool
* @deprecated since Moodle 4.0 MDL-71585
* @see qbank_managecategories\helper
* @todo Final deprecation on Moodle 4.4 MDL-72438
*/
function question_is_top_category($categoryid) {
global $DB;
return 0 == $DB->get_field('question_categories', 'parent', array('id' => $categoryid));
debugging('Function question_is_top_category() has been deprecated and moved to qbank_managecategories plugin,
Please use qbank_managecategories\helper::question_is_top_category() instead.', DEBUG_DEVELOPER);
return \qbank_managecategories\helper::question_is_top_category($categoryid);
}
/**
* Ensures that this user is allowed to delete this category.
*
* @param int $todelete a category id.
* @deprecated since Moodle 4.0 MDL-71585
* @see qbank_managecategories\helper
* @todo Final deprecation on Moodle 4.4 MDL-72438
*/
function question_can_delete_cat($todelete) {
global $DB;
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));
require_capability('moodle/question:managecategory', context::instance_by_id($contextid));
}
debugging('Function question_can_delete_cat() has been deprecated and moved to qbank_managecategories plugin,
Please use qbank_managecategories\helper::question_can_delete_cat() instead.', DEBUG_DEVELOPER);
\qbank_managecategories\helper::question_can_delete_cat($todelete);
}

View File

@ -1065,7 +1065,7 @@ class qformat_default {
* back into an array of category names.
*
* Each category name is cleaned by a call to clean_param(, PARAM_TEXT),
* which matches the cleaning in question/category_form.php.
* which matches the cleaning in question/bank/managecategories/category_form.php.
*
* @param string $path
* @return array of category names.

View File

@ -34,9 +34,14 @@ require_once($CFG->libdir . '/formslib.php');
*
* @copyright 2008 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @deprecated since Moodle 4.0 MDL-71585
* @see qbank_managecategories\form\question_move_form
*/
class question_move_form extends moodleform {
protected function definition() {
debugging('Class question_move_form in \core_question\move_form is deprecated,
please use qbank_managecategories\form\question_move_form instead.', DEBUG_DEVELOPER);
$mform = $this->_form;
$currentcat = $this->_customdata['currentcat'];

View File

@ -1,8 +1,8 @@
@core @core_question @javascript
Feature: A teacher can put questions in categories in the question bank
@core @core_question
Feature: A teacher can move questions between categories in the question bank
In order to organize my questions
As a teacher
I create and edit categories and move questions between them
I move questions between categories
Background:
Given the following "users" exist:
@ -26,42 +26,6 @@ Feature: A teacher can put questions in categories in the question bank
And I log in as "teacher1"
And I am on "Course 1" course homepage
Scenario: A new question category can be created
When I navigate to "Question bank > Categories" in current page administration
And I set the following fields to these values:
| Name | New Category 1 |
| Parent category | Top |
| Category info | Created as a test |
| ID number | newcatidnumber |
And I press "submitbutton"
Then I should see "New Category 1 ID number newcatidnumber (0)"
And I should see "Created as a test" in the "New Category 1" "list_item"
And "New Category 1 [newcatidnumber]" "option" should exist in the "Parent category" "select"
Scenario: A question category can be edited
When I navigate to "Question bank > Categories" in current page administration
And I click on "Edit this category" "link" in the "Subcategory" "list_item"
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 |
And I press "Save changes"
Then I should see "New name"
And I should see "I was edited" in the "New name" "list_item"
Scenario: An empty question category can be deleted
When I navigate to "Question bank > Categories" in current page administration
And I click on "Delete" "link" in the "Subcategory" "list_item"
Then I should not see "Subcategory"
Scenario: An non-empty question category can be deleted if you move the contents elsewhere
When I navigate to "Question bank > Categories" in current page administration
And I click on "Delete" "link" in the "Used category" "list_item"
And I should see "The category 'Used category' contains 1 questions"
And I press "Save in category"
Then I should not see "Used category"
And I should see "Default for C1 (1)"
@javascript
Scenario: Move a question between categories via the question page
When I navigate to "Question bank > Questions" in current page administration

View File

@ -1,8 +1,8 @@
@core @core_question
Feature: A teacher can put questions with idnumbers in categories with idnumbers in the question bank
Feature: A teacher can put questions with idnumbers in categories in the question bank
In order to organize my questions
As a teacher
I create and edit categories and move questions between them (now with idnumbers)
I move questions between categories (now with idnumbers)
Background:
Given the following "users" exist:
@ -17,37 +17,6 @@ Feature: A teacher can put questions with idnumbers in categories with idnumbers
And I log in as "teacher1"
And I am on "Course 1" course homepage
Scenario: A new question category can only be created with a unique idnumber for a context
# Note need to create the top category each time.
When the following "question categories" exist:
| contextlevel | reference | questioncategory | name | idnumber |
| Course | C1 | Top | top | |
| Course | C1 | top | Used category | c1used |
And I navigate to "Question bank > Categories" in current page administration
And I set the following fields to these values:
| Name | Sub used category |
| Parent category | Used category |
| Category info | Created as a test |
| ID number | c1used |
And I press "Add category"
# Standard warning.
Then I should see "This ID number is already in use"
# Correction to a unique idnumber for the context.
And I set the field "ID number" to "c1unused"
And I press "Add category"
Then I should see "Sub used category ID number c1unused (0)"
And I should see "Created as a test" in the "Sub used category" "list_item"
Scenario: A question category can be edited and saved without changing the idnumber
When the following "question categories" exist:
| contextlevel | reference | questioncategory | name | idnumber |
| Course | C1 | Top | top | |
| Course | C1 | top | Used category | c1used |
And I navigate to "Question bank > Categories" in current page administration
And I click on "Edit this category" "link" in the "Used category" "list_item"
And I press "Save changes"
Then I should not see "This ID number is already in use"
Scenario: A question can only have a unique idnumber within a category
When the following "question categories" exist:
| contextlevel | reference | questioncategory | name | idnumber |

View File

@ -1,178 +0,0 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Events tests.
*
* @package core_question
* @copyright 2019 the Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/question/editlib.php');
require_once($CFG->dirroot . '/question/category_class.php');
class core_question_category_class_testcase extends advanced_testcase {
/**
* @var question_category_object used in the tests.
*/
protected $qcobject;
/**
* @var context a context to use.
*/
protected $context;
/**
* @var stdClass top category in context.
*/
protected $topcat;
protected function setUp(): void {
parent::setUp();
self::setAdminUser();
$this->resetAfterTest();
$this->context = context_course::instance(SITEID);
$contexts = new question_edit_contexts($this->context);
$this->topcat = question_get_top_category($this->context->id, true);
$this->qcobject = new question_category_object(null,
new moodle_url('/question/category.php', ['courseid' => SITEID]),
$contexts->having_one_edit_tab_cap('categories'), 0, null, 0,
$contexts->having_cap('moodle/question:add'));
}
/**
* Test creating a category.
*/
public function test_add_category_no_idnumber() {
global $DB;
$id = $this->qcobject->add_category($this->topcat->id . ',' . $this->topcat->contextid,
'New category', '', true, FORMAT_HTML, ''); // No idnumber passed as '' to match form data.
$newcat = $DB->get_record('question_categories', ['id' => $id], '*', MUST_EXIST);
$this->assertSame('New category', $newcat->name);
$this->assertNull($newcat->idnumber);
}
/**
* Test creating a category with a tricky idnumber.
*/
public function test_add_category_set_idnumber_0() {
global $DB;
$id = $this->qcobject->add_category($this->topcat->id . ',' . $this->topcat->contextid,
'New category', '', true, FORMAT_HTML, '0');
$newcat = $DB->get_record('question_categories', ['id' => $id], '*', MUST_EXIST);
$this->assertSame('New category', $newcat->name);
$this->assertSame('0', $newcat->idnumber);
}
/**
* Trying to add a category with duplicate idnumber blanks it.
* (In reality, this would probably get caught by form validation.)
*/
public function test_add_category_try_to_set_duplicate_idnumber() {
global $DB;
$this->qcobject->add_category($this->topcat->id . ',' . $this->topcat->contextid,
'Existing category', '', true, FORMAT_HTML, 'frog');
$id = $this->qcobject->add_category($this->topcat->id . ',' . $this->topcat->contextid,
'New category', '', true, FORMAT_HTML, 'frog');
$newcat = $DB->get_record('question_categories', ['id' => $id], '*', MUST_EXIST);
$this->assertSame('New category', $newcat->name);
$this->assertNull($newcat->idnumber);
}
/**
* Test updating a category.
*/
public function test_update_category() {
global $DB;
$id = $this->qcobject->add_category($this->topcat->id . ',' . $this->topcat->contextid,
'Old name', 'Description', true, FORMAT_HTML, 'frog');
$this->qcobject->update_category($id, $this->topcat->id . ',' . $this->topcat->contextid,
'New name', 'New description', FORMAT_HTML, '0', false);
$newcat = $DB->get_record('question_categories', ['id' => $id], '*', MUST_EXIST);
$this->assertSame('New name', $newcat->name);
$this->assertSame('0', $newcat->idnumber);
}
/**
* Test updating a category to remove the idnumber.
*/
public function test_update_category_removing_idnumber() {
global $DB;
$id = $this->qcobject->add_category($this->topcat->id . ',' . $this->topcat->contextid,
'Old name', 'Description', true, FORMAT_HTML, 'frog');
$this->qcobject->update_category($id, $this->topcat->id . ',' . $this->topcat->contextid,
'New name', 'New description', FORMAT_HTML, '', false);
$newcat = $DB->get_record('question_categories', ['id' => $id], '*', MUST_EXIST);
$this->assertSame('New name', $newcat->name);
$this->assertNull($newcat->idnumber);
}
/**
* Test updating a category without changing the idnumber.
*/
public function test_update_category_dont_change_idnumber() {
global $DB;
$id = $this->qcobject->add_category($this->topcat->id . ',' . $this->topcat->contextid,
'Old name', 'Description', true, FORMAT_HTML, 'frog');
$this->qcobject->update_category($id, $this->topcat->id . ',' . $this->topcat->contextid,
'New name', 'New description', FORMAT_HTML, 'frog', false);
$newcat = $DB->get_record('question_categories', ['id' => $id], '*', MUST_EXIST);
$this->assertSame('New name', $newcat->name);
$this->assertSame('frog', $newcat->idnumber);
}
/**
* Trying to update a category so its idnumber duplicates idnumber blanks it.
* (In reality, this would probably get caught by form validation.)
*/
public function test_update_category_try_to_set_duplicate_idnumber() {
global $DB;
$this->qcobject->add_category($this->topcat->id . ',' . $this->topcat->contextid,
'Existing category', '', true, FORMAT_HTML, 'toad');
$id = $this->qcobject->add_category($this->topcat->id . ',' . $this->topcat->contextid,
'old name', '', true, FORMAT_HTML, 'frog');
$this->qcobject->update_category($id, $this->topcat->id . ',' . $this->topcat->contextid,
'New name', '', FORMAT_HTML, 'toad', false);
$newcat = $DB->get_record('question_categories', ['id' => $id], '*', MUST_EXIST);
$this->assertSame('New name', $newcat->name);
$this->assertNull($newcat->idnumber);
}
}

View File

@ -27,7 +27,8 @@ defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/question/editlib.php');
require_once($CFG->dirroot . '/question/category_class.php');
use qbank_managecategories\question_category_object;
class core_question_events_testcase extends advanced_testcase {
@ -38,166 +39,6 @@ class core_question_events_testcase extends advanced_testcase {
$this->resetAfterTest();
}
/**
* Test the question category created event.
*/
public function test_question_category_created() {
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
$quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id]);
$contexts = new question_edit_contexts(context_module::instance($quiz->cmid));
$defaultcategoryobj = question_make_default_categories([$contexts->lowest()]);
$defaultcategory = $defaultcategoryobj->id . ',' . $defaultcategoryobj->contextid;
$qcobject = new question_category_object(
1,
new moodle_url('/mod/quiz/edit.php', ['cmid' => $quiz->cmid]),
$contexts->having_one_edit_tab_cap('categories'),
$defaultcategoryobj->id,
$defaultcategory,
null,
$contexts->having_cap('moodle/question:add'));
// Trigger and capture the event.
$sink = $this->redirectEvents();
$categoryid = $qcobject->add_category($defaultcategory, 'newcategory', '', true);
$events = $sink->get_events();
$event = reset($events);
// Check that the event data is valid.
$this->assertInstanceOf('\core\event\question_category_created', $event);
$this->assertEquals(context_module::instance($quiz->cmid), $event->get_context());
$expected = [$course->id, 'quiz', 'addcategory', 'view.php?id=' . $quiz->cmid , $categoryid, $quiz->cmid];
$this->assertEventLegacyLogData($expected, $event);
$this->assertEventContextNotUsed($event);
}
/**
* Test the question category deleted event.
*/
public function test_question_category_deleted() {
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
$quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id]);
$contexts = new question_edit_contexts(context_module::instance($quiz->cmid));
$defaultcategoryobj = question_make_default_categories([$contexts->lowest()]);
$defaultcategory = $defaultcategoryobj->id . ',' . $defaultcategoryobj->contextid;
$qcobject = new question_category_object(
1,
new moodle_url('/mod/quiz/edit.php', ['cmid' => $quiz->cmid]),
$contexts->having_one_edit_tab_cap('categories'),
$defaultcategoryobj->id,
$defaultcategory,
null,
$contexts->having_cap('moodle/question:add'));
// Create the category.
$categoryid = $qcobject->add_category($defaultcategory, 'newcategory', '', true);
// Trigger and capture the event.
$sink = $this->redirectEvents();
$qcobject->delete_category($categoryid);
$events = $sink->get_events();
$event = reset($events);
// Check that the event data is valid.
$this->assertInstanceOf('\core\event\question_category_deleted', $event);
$this->assertEquals(context_module::instance($quiz->cmid), $event->get_context());
$this->assertEquals($categoryid, $event->objectid);
$this->assertDebuggingNotCalled();
}
/**
* Test the question category updated event.
*/
public function test_question_category_updated() {
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
$quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id]);
$contexts = new question_edit_contexts(context_module::instance($quiz->cmid));
$defaultcategoryobj = question_make_default_categories([$contexts->lowest()]);
$defaultcategory = $defaultcategoryobj->id . ',' . $defaultcategoryobj->contextid;
$qcobject = new question_category_object(
1,
new moodle_url('/mod/quiz/edit.php', ['cmid' => $quiz->cmid]),
$contexts->having_one_edit_tab_cap('categories'),
$defaultcategoryobj->id,
$defaultcategory,
null,
$contexts->having_cap('moodle/question:add'));
// Create the category.
$categoryid = $qcobject->add_category($defaultcategory, 'newcategory', '', true);
// Trigger and capture the event.
$sink = $this->redirectEvents();
$qcobject->update_category($categoryid, $defaultcategory, 'updatedcategory', '', FORMAT_HTML, '', false);
$events = $sink->get_events();
$event = reset($events);
// Check that the event data is valid.
$this->assertInstanceOf('\core\event\question_category_updated', $event);
$this->assertEquals(context_module::instance($quiz->cmid), $event->get_context());
$this->assertEquals($categoryid, $event->objectid);
$this->assertDebuggingNotCalled();
}
/**
* Test the question category viewed event.
* There is no external API for viewing the category, so the unit test will simply
* create and trigger the event and ensure data is returned as expected.
*/
public function test_question_category_viewed() {
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
$quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id]);
$contexts = new question_edit_contexts(context_module::instance($quiz->cmid));
$defaultcategoryobj = question_make_default_categories([$contexts->lowest()]);
$defaultcategory = $defaultcategoryobj->id . ',' . $defaultcategoryobj->contextid;
$qcobject = new question_category_object(
1,
new moodle_url('/mod/quiz/edit.php', ['cmid' => $quiz->cmid]),
$contexts->having_one_edit_tab_cap('categories'),
$defaultcategoryobj->id,
$defaultcategory,
null,
$contexts->having_cap('moodle/question:add'));
// Create the category.
$categoryid = $qcobject->add_category($defaultcategory, 'newcategory', '', true);
// Log the view of this category.
$category = new stdClass();
$category->id = $categoryid;
$context = context_module::instance($quiz->cmid);
$event = \core\event\question_category_viewed::create_from_question_category_instance($category, $context);
// Trigger and capture the event.
$sink = $this->redirectEvents();
$event->trigger();
$events = $sink->get_events();
$event = reset($events);
// Check that the event data is valid.
$this->assertInstanceOf('\core\event\question_category_viewed', $event);
$this->assertEquals(context_module::instance($quiz->cmid), $event->get_context());
$this->assertEquals($categoryid, $event->objectid);
$this->assertDebuggingNotCalled();
}
/**
* Test the questions imported event.
* There is no easy way to trigger this event using the API, so the unit test will simply