+
{{#str}}match{{/str}}
-
+
{{#str}}none{{/str}}
{{#str}}any{{/str}}
{{#str}}all{{/str}}
-
{{#str}}filtertype{{/str}}
-
+ {{#str}}filtertype{{/str}}
+
{{#str}}selectfiltertype{{/str}}
{{#filtertypes}}
{{title}}
{{/filtertypes}}
-
+
{{#pix}}e/cancel_solid_circle{{/pix}}
diff --git a/lib/templates/datafilter/filter_type.mustache b/lib/templates/datafilter/filter_type.mustache
index 8fae28768d9..34ef3de26e3 100644
--- a/lib/templates/datafilter/filter_type.mustache
+++ b/lib/templates/datafilter/filter_type.mustache
@@ -52,10 +52,12 @@
}}data-field-name="{{name}}" {{!
}}data-field-title="{{title}}" {{!
}}data-allow-custom="{{allowcustom}}" {{!
+ }}data-required="{{required}}" {{!
+ }}data-join-list="{{joinlist}}" {{!
}}class="hidden" {{!
}}{{#filtertypeclass}}data-filter-type-class="{{filtertypeclass}}" {{/filtertypeclass}}{{!
}}>
{{#values}}
- {{{title}}}
+ {{{title}}}
{{/values}}
diff --git a/lib/templates/datafilter/filtertypes/binary_selector.mustache b/lib/templates/datafilter/filtertypes/binary_selector.mustache
new file mode 100644
index 00000000000..6d48da0920f
--- /dev/null
+++ b/lib/templates/datafilter/filtertypes/binary_selector.mustache
@@ -0,0 +1,46 @@
+{{!
+ 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
.
+}}
+{{!
+ @template core/datafilter/filtertypes/binary_selector
+
+ Moodle template for hidden, questiontext and subcategories filter.
+
+ Context variables required for this template:
+ * filtertype - filter type name for dataset filterfield.
+ * textvalueone - text value for first selection.
+ * textvaluetwo - text value for second selection.
+ * optionone - boolen for first option selection.
+ * optiontwo - boolean for second option selection.
+
+ Example context (json):
+ {
+ "title": "Show hidden questions",
+ "filtertype": "hidden",
+ "textvalueone": "yes",
+ "textvaluetwo": "no",
+ "optionone": false,
+ "optiontwo": true
+ }
+}}
+
+ {{title}}
+
+ {{#options}}
+ {{text}}
+ {{/options}}
+
+
diff --git a/lib/templates/form_autocomplete_suggestions.mustache b/lib/templates/form_autocomplete_suggestions.mustache
index 7e1a0f58687..e619b7d26e7 100644
--- a/lib/templates/form_autocomplete_suggestions.mustache
+++ b/lib/templates/form_autocomplete_suggestions.mustache
@@ -32,11 +32,12 @@
Example context (json):
{ "suggestionsId": 1, "options": [
{ "label": "Item label with
tags ", "value": "5" },
- { "label": "Another item label with
tags ", "value": "4" }
+ { "label": "Another item label with
tags ", "value": "4"},
+ { "label": "A disabled item", "value": "", "disabled": true}
]}
}}
diff --git a/lib/upgrade.txt b/lib/upgrade.txt
index 942d6e886b6..493373c8a05 100644
--- a/lib/upgrade.txt
+++ b/lib/upgrade.txt
@@ -145,6 +145,19 @@ being forced open in all behat tests.
- `$fullformat` - Passing `true` will return date/time units in full format (e.g. `1 day` instead of `1d`).
* The print_object function (for temporary debugging use only) can now be used on objects that have recursive references,
and has an improved display and other changes. (New parameters are available; see function documentation.)
+* New `filteroptions` field added to getFilterValue() in core/datafilter/filtertype. This is generated by `get filterOptions()`
+ and returns an arbitrary list of options specific to the filter type, in [{name:, value}] format. This allows filters to extend
+ the values returned with additional fields.
+* New `required` field added to core\output\datafilter. This will be output via the data-required property in filter_type.mustache,
+ any additional filter types will need to output this property as well. Filters with data-required="1" will not be removable from
+ the list of filters.
+* addFilterRow() in core/datafilter now accepts a filterdata object to add a row with a pre-defined filter.
+* New "binary" datafilter type added for creating filters with a single yes/no option.
+* New "joinlist" parameter added to core\output\datafilter::get_filter_object(). This takes an array of datafilter::JOINTYPE_*
+ constants to define which types of join the filter supports. By default this will include all three of "Any", "All" and "None",
+ but a subset can be specified instead.
+* core/form-autocomplete now supports disabled options in the source select list. These will be displayed in the autocomplete
+ options with the aria-disabled attribute, and will not be selectable.
=== 4.2 ===
diff --git a/mod/quiz/addrandom.php b/mod/quiz/addrandom.php
deleted file mode 100644
index 09862164abf..00000000000
--- a/mod/quiz/addrandom.php
+++ /dev/null
@@ -1,141 +0,0 @@
-.
-
-/**
- * Fallback page of /mod/quiz/edit.php add random question dialog,
- * for users who do not use javascript.
- *
- * @package mod_quiz
- * @copyright 2008 Olli Savolainen
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-
-require_once(__DIR__ . '/../../config.php');
-require_once($CFG->dirroot . '/mod/quiz/locallib.php');
-require_once($CFG->dirroot . '/question/editlib.php');
-
-use mod_quiz\form\add_random_form;
-use mod_quiz\quiz_settings;
-use qbank_managecategories\question_category_object;
-
-list($thispageurl, $contexts, $cmid, $cm, $quiz, $pagevars) =
- question_edit_setup('editq', '/mod/quiz/addrandom.php', true);
-
-// These params are only passed from page request to request while we stay on
-// this page otherwise they would go in question_edit_setup.
-$returnurl = optional_param('returnurl', '', PARAM_LOCALURL);
-$addonpage = optional_param('addonpage', 0, PARAM_INT);
-$category = optional_param('category', 0, PARAM_INT);
-$mdlscrollto = optional_param('mdlscrollto', 0, PARAM_INT);
-
-$quizobj = quiz_settings::create($quiz->id);
-$course = $quizobj->get_course();
-
-// You need mod/quiz:manage in addition to question capabilities to access this page.
-// You also need the moodle/question:useall capability somewhere.
-require_capability('mod/quiz:manage', $contexts->lowest());
-if (!$contexts->having_cap('moodle/question:useall')) {
- throw new \moodle_exception('nopermissions', '', '', 'use');
-}
-
-$PAGE->set_url($thispageurl);
-
-if ($returnurl) {
- $returnurl = new moodle_url($returnurl);
-} else {
- $returnurl = new moodle_url('/mod/quiz/edit.php', ['cmid' => $cmid]);
-}
-if ($mdlscrollto) {
- $returnurl->param('mdlscrollto', $mdlscrollto);
-}
-
-$defaultcategoryobj = question_make_default_categories($contexts->all());
-$defaultcategory = $defaultcategoryobj->id . ',' . $defaultcategoryobj->contextid;
-
-$qcobject = new question_category_object(
- $pagevars['cpage'],
- $thispageurl,
- $contexts->having_one_edit_tab_cap('categories'),
- $defaultcategoryobj->id,
- $defaultcategory,
- null,
- $contexts->having_cap('moodle/question:add'));
-
-$mform = new add_random_form(new moodle_url('/mod/quiz/addrandom.php'),
- ['contexts' => $contexts, 'cat' => $pagevars['cat']]);
-
-if ($mform->is_cancelled()) {
- redirect($returnurl);
-}
-
-if ($data = $mform->get_data()) {
- if (!empty($data->existingcategory)) {
- list($categoryid) = explode(',', $data->category);
- $includesubcategories = !empty($data->includesubcategories);
- if (!$includesubcategories) {
- // If the chosen category is a top category.
- $includesubcategories = $DB->record_exists('question_categories', ['id' => $categoryid, 'parent' => 0]);
- }
- $returnurl->param('cat', $data->category);
-
- } else if (!empty($data->newcategory)) {
- list($parentid, $contextid) = explode(',', $data->parent);
- $categoryid = $qcobject->add_category($data->parent, $data->name, '', true);
- $includesubcategories = 0;
-
- $returnurl->param('cat', $categoryid . ',' . $contextid);
- } else {
- throw new coding_exception(
- 'It seems a form was submitted without any button being pressed???');
- }
-
- if (empty($data->fromtags)) {
- $data->fromtags = [];
- }
-
- $tagids = array_map(function($tagstrings) {
- return (int)explode(',', $tagstrings)[0];
- }, $data->fromtags);
-
- quiz_add_random_questions($quiz, $addonpage, $categoryid, $data->numbertoadd, $includesubcategories, $tagids);
- quiz_delete_previews($quiz);
- $quizobj->get_grade_calculator()->recompute_quiz_sumgrades();
- redirect($returnurl);
-}
-
-$mform->set_data([
- 'addonpage' => $addonpage,
- 'returnurl' => $returnurl,
- 'cmid' => $cm->id,
- 'category' => $category,
-]);
-
-// Setup $PAGE.
-$streditingquiz = get_string('editinga', 'moodle', get_string('modulename', 'quiz'));
-$PAGE->navbar->add($streditingquiz);
-$PAGE->set_title($streditingquiz);
-$PAGE->set_heading($course->fullname);
-echo $OUTPUT->header();
-
-if (!$quizname = $DB->get_field($cm->modname, 'name', ['id' => $cm->instance])) {
- throw new \moodle_exception('invalidcoursemodule');
-}
-
-echo $OUTPUT->heading(get_string('addrandomquestiontoquiz', 'quiz', $quizname), 2);
-$mform->display();
-echo $OUTPUT->footer();
-
diff --git a/mod/quiz/amd/build/modal_add_random_question.min.js b/mod/quiz/amd/build/modal_add_random_question.min.js
index a33960fe2e9..1c0f5a3d5a0 100644
--- a/mod/quiz/amd/build/modal_add_random_question.min.js
+++ b/mod/quiz/amd/build/modal_add_random_question.min.js
@@ -1,3 +1,3 @@
-define("mod_quiz/modal_add_random_question",["exports","jquery","core/modal","core/notification","core/fragment","core/templates","core_form/changechecker"],(function(_exports,_jquery,_modal,Notification,Fragment,Templates,FormChangeChecker){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_jquery=_interopRequireDefault(_jquery),_modal=_interopRequireDefault(_modal),Notification=_interopRequireWildcard(Notification),Fragment=_interopRequireWildcard(Fragment),Templates=_interopRequireWildcard(Templates),FormChangeChecker=_interopRequireWildcard(FormChangeChecker);const SELECTORS_EXISTING_CATEGORY_CONTAINER='[data-region="existing-category-container"]',SELECTORS_EXISTING_CATEGORY_FORM_ELEMENT="#id_existingcategoryheader",SELECTORS_NEW_CATEGORY_CONTAINER='[data-region="new-category-container"]',SELECTORS_NEW_CATEGORY_FORM_ELEMENT="#id_newcategoryheader",SELECTORS_TAB_CONTENT='[data-region="tab-content"]',SELECTORS_ADD_ON_PAGE_FORM_ELEMENT='[name="addonpage"]',SELECTORS_SUBMIT_BUTTON_ELEMENT='input[type="submit"]',SELECTORS_CANCEL_BUTTON_ELEMENT='input[type="submit"][name="cancel"]',SELECTORS_FORM_HEADER="legend",SELECTORS_BUTTON_CONTAINER=".fitem";class ModalAddRandomQuestion extends _modal.default{constructor(root){super(root),this.contextId=null,this.addOnPageId=null,this.category=null,this.returnUrl=null,this.cmid=null,this.loadedForm=!1}setContextId(id){this.contextId=id}getContextId(){return this.contextId}setAddOnPageId(id){this.addOnPageId=id,this.getBody().find(SELECTORS_ADD_ON_PAGE_FORM_ELEMENT).val(id)}getAddOnPageId(){return this.addOnPageId}setCategory(category){this.category=category}getCategory(){return this.category}setReturnUrl(url){this.returnUrl=url}getReturnUrl(){return this.returnUrl}setCMID(id){this.cmid=id}getCMID(){return this.cmid}moveFormElementIntoTab(formElement,tabElement){const submitButtons=formElement.find(SELECTORS_SUBMIT_BUTTON_ELEMENT),footer=(0,_jquery.default)('');formElement.find(SELECTORS_FORM_HEADER).addClass("hidden"),formElement.wrap(tabElement),submitButtons.closest(SELECTORS_BUTTON_CONTAINER).remove(),submitButtons.appendTo(footer),footer.appendTo(formElement)}moveTabsIntoTabContent(form){const tabContent=this.getBody().find(SELECTORS_TAB_CONTENT).empty();form.find('[role="tabpanel"]').wrapAll(tabContent)}moveCancelButtonToTabs(form){const cancelButton=form.find(SELECTORS_CANCEL_BUTTON_ELEMENT).addClass("ml-1"),tabFooters=form.find('[data-region="footer"]');cancelButton.closest(SELECTORS_BUTTON_CONTAINER).remove(),cancelButton.clone().appendTo(tabFooters)}loadForm(){return Fragment.loadFragment("mod_quiz","add_random_question_form",this.getContextId(),{addonpage:this.getAddOnPageId(),cat:this.getCategory(),returnurl:this.getReturnUrl(),cmid:this.getCMID()}).then(((html,js)=>{const form=(0,_jquery.default)(html),existingCategoryFormElement=form.find(SELECTORS_EXISTING_CATEGORY_FORM_ELEMENT),existingCategoryTab=this.getBody().find(SELECTORS_EXISTING_CATEGORY_CONTAINER),newCategoryFormElement=form.find(SELECTORS_NEW_CATEGORY_FORM_ELEMENT),newCategoryTab=this.getBody().find(SELECTORS_NEW_CATEGORY_CONTAINER);this.moveFormElementIntoTab(existingCategoryFormElement,existingCategoryTab),this.moveFormElementIntoTab(newCategoryFormElement,newCategoryTab),this.moveTabsIntoTabContent(form),this.moveCancelButtonToTabs(form),Templates.replaceNode(this.getBody().find(SELECTORS_TAB_CONTENT),form,js)})).then((()=>{FormChangeChecker.disableAllChecks()})).catch(Notification.exception)}show(){super.show(this),this.loadedForm||(this.loadForm(),this.loadedForm=!0)}}return _exports.default=ModalAddRandomQuestion,_defineProperty(ModalAddRandomQuestion,"TYPE","mod_quiz-quiz-add-random-question"),_defineProperty(ModalAddRandomQuestion,"TEMPLATE","mod_quiz/modal_add_random_question"),ModalAddRandomQuestion.registerModalType(),_exports.default}));
+define("mod_quiz/modal_add_random_question",["exports","jquery","core/modal","core/notification","core/fragment","core/templates","core_form/changechecker","core/ajax"],(function(_exports,_jquery,_modal,Notification,Fragment,Templates,FormChangeChecker,_ajax){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_jquery=_interopRequireDefault(_jquery),_modal=_interopRequireDefault(_modal),Notification=_interopRequireWildcard(Notification),Fragment=_interopRequireWildcard(Fragment),Templates=_interopRequireWildcard(Templates),FormChangeChecker=_interopRequireWildcard(FormChangeChecker);const SELECTORS={EXISTING_CATEGORY_CONTAINER:'[data-region="existing-category-container"]',EXISTING_CATEGORY_TAB:"#id_existingcategoryheader",NEW_CATEGORY_CONTAINER:'[data-region="new-category-container"]',NEW_CATEGORY_TAB:"#id_newcategoryheader",TAB_CONTENT:'[data-region="tab-content"]',ADD_ON_PAGE_FORM_ELEMENT:'[name="addonpage"]',ADD_RANDOM_BUTTON:'input[type="submit"][name="addrandom"]',ADD_NEW_CATEGORY_BUTTON:'input[type="submit"][name="newcategory"]',SUBMIT_BUTTON_ELEMENT:'input[type="submit"][name="addrandom"], input[type="submit"][name="newcategory"]',FORM_HEADER:"legend",SELECT_NUMBER_TO_ADD:"#menurandomcount",NEW_CATEGORY_ELEMENT:"#categoryname",PARENT_CATEGORY_ELEMENT:"#parentcategory",FILTER_CONDITION_ELEMENT:"[data-filtercondition]",FORM_ELEMENT:"#add_random_question_form",MESSAGE_INPUT:'[name="message"]'};class ModalAddRandomQuestion extends _modal.default{constructor(root){super(root),this.contextId=null,this.addOnPageId=null,this.category=null,this.returnUrl=null,this.cmid=null,this.loadedForm=!1}setContextId(id){this.contextId=id}getContextId(){return this.contextId}setAddOnPageId(id){this.addOnPageId=id,this.getBody().find(SELECTORS.ADD_ON_PAGE_FORM_ELEMENT).val(id)}getAddOnPageId(){return this.addOnPageId}setCategory(category){this.category=category}getCategory(){return this.category}setReturnUrl(url){this.returnUrl=url}getReturnUrl(){return this.returnUrl}setCMID(id){this.cmid=id}getCMID(){return this.cmid}moveContentIntoTab(tabContent,tabElement){tabContent.find(SELECTORS.FORM_HEADER).addClass("hidden"),tabContent.wrap(tabElement)}moveTabsIntoTabContent(form){const tabContent=this.getBody().find(SELECTORS.TAB_CONTENT).empty();form.find('[role="tabpanel"]').wrapAll(tabContent)}moveCancelButtonToTabs(form){const cancelButton=form.find(SELECTORS.CANCEL_BUTTON_ELEMENT).addClass("ml-1"),tabFooters=form.find('[data-region="footer"]');cancelButton.closest(SELECTORS.BUTTON_CONTAINER).remove(),cancelButton.clone().appendTo(tabFooters)}loadForm(){const cmid=this.getCMID(),cat=this.getCategory(),addonpage=this.getAddOnPageId(),returnurl=this.getReturnUrl();return Fragment.loadFragment("mod_quiz","add_random_question_form",this.getContextId(),{addonpage:addonpage,cat:cat,returnurl:returnurl,cmid:cmid}).then(((html,js)=>{const form=(0,_jquery.default)(html),existingCategoryTabContent=form.find(SELECTORS.EXISTING_CATEGORY_TAB),existingCategoryTab=this.getBody().find(SELECTORS.EXISTING_CATEGORY_CONTAINER),newCategoryTabContent=form.find(SELECTORS.NEW_CATEGORY_TAB),newCategoryTab=this.getBody().find(SELECTORS.NEW_CATEGORY_CONTAINER);this.moveContentIntoTab(existingCategoryTabContent,existingCategoryTab),this.moveContentIntoTab(newCategoryTabContent,newCategoryTab),this.moveTabsIntoTabContent(form),Templates.replaceNode(this.getBody().find(SELECTORS.TAB_CONTENT),form,js)})).then((()=>{FormChangeChecker.disableAllChecks(),this.getBody()[0].addEventListener("click",(e=>{var _document$querySelect;if(!e.target.closest(SELECTORS.SUBMIT_BUTTON_ELEMENT))return;e.preventDefault();const randomcount=document.querySelector(SELECTORS.SELECT_NUMBER_TO_ADD).value,filtercondition=null===(_document$querySelect=document.querySelector(SELECTORS.FILTER_CONDITION_ELEMENT).dataset)||void 0===_document$querySelect?void 0:_document$querySelect.filtercondition;if(e.target.closest(SELECTORS.ADD_RANDOM_BUTTON))return void this.addQuestions(cmid,addonpage,randomcount,filtercondition,"","");e.target.closest(SELECTORS.ADD_NEW_CATEGORY_BUTTON)&&this.addQuestions(cmid,addonpage,randomcount,filtercondition,document.querySelector(SELECTORS.NEW_CATEGORY_ELEMENT).value,document.querySelector(SELECTORS.PARENT_CATEGORY_ELEMENT).value)}))})).catch(Notification.exception)}async addQuestions(cmid,addonpage,randomcount,filtercondition,newcategory,parentcategory){const call={methodname:"mod_quiz_add_random_questions",args:{cmid:cmid,addonpage:addonpage,randomcount:randomcount,filtercondition:filtercondition,newcategory:newcategory,parentcategory:parentcategory}};try{const response=await(0,_ajax.call)([call])[0],form=document.querySelector(SELECTORS.FORM_ELEMENT);form.querySelector(SELECTORS.MESSAGE_INPUT).value=response.message,form.submit()}catch(e){Notification.exception(e)}}show(){super.show(this),this.loadedForm||(this.loadForm(window.location.search),this.loadedForm=!0)}}return _exports.default=ModalAddRandomQuestion,_defineProperty(ModalAddRandomQuestion,"TYPE","mod_quiz-quiz-add-random-question"),_defineProperty(ModalAddRandomQuestion,"TEMPLATE","mod_quiz/modal_add_random_question"),ModalAddRandomQuestion.registerModalType(),_exports.default}));
//# sourceMappingURL=modal_add_random_question.min.js.map
\ No newline at end of file
diff --git a/mod/quiz/amd/build/modal_add_random_question.min.js.map b/mod/quiz/amd/build/modal_add_random_question.min.js.map
index 728f80bcca9..9ec71431ca5 100644
--- a/mod/quiz/amd/build/modal_add_random_question.min.js.map
+++ b/mod/quiz/amd/build/modal_add_random_question.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"modal_add_random_question.min.js","sources":["../src/modal_add_random_question.js"],"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
.\n\n/**\n * Contain the logic for the add random question modal.\n *\n * @module mod_quiz/modal_add_random_question\n * @copyright 2018 Ryan Wyllie
\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport Modal from 'core/modal';\nimport * as Notification from 'core/notification';\nimport * as Fragment from 'core/fragment';\nimport * as Templates from 'core/templates';\nimport * as FormChangeChecker from 'core_form/changechecker';\n\nconst SELECTORS = {\n EXISTING_CATEGORY_CONTAINER: '[data-region=\"existing-category-container\"]',\n EXISTING_CATEGORY_FORM_ELEMENT: '#id_existingcategoryheader',\n NEW_CATEGORY_CONTAINER: '[data-region=\"new-category-container\"]',\n NEW_CATEGORY_FORM_ELEMENT: '#id_newcategoryheader',\n TAB_CONTENT: '[data-region=\"tab-content\"]',\n ADD_ON_PAGE_FORM_ELEMENT: '[name=\"addonpage\"]',\n SUBMIT_BUTTON_ELEMENT: 'input[type=\"submit\"]',\n CANCEL_BUTTON_ELEMENT: 'input[type=\"submit\"][name=\"cancel\"]',\n FORM_HEADER: 'legend',\n BUTTON_CONTAINER: '.fitem',\n};\n\nexport default class ModalAddRandomQuestion extends Modal {\n static TYPE = 'mod_quiz-quiz-add-random-question';\n static TEMPLATE = 'mod_quiz/modal_add_random_question';\n\n /**\n * Constructor for the Modal.\n *\n * @param {object} root The root jQuery element for the modal\n */\n constructor(root) {\n super(root);\n this.contextId = null;\n this.addOnPageId = null;\n this.category = null;\n this.returnUrl = null;\n this.cmid = null;\n this.loadedForm = false;\n }\n\n /**\n * Save the Moodle context id that the question bank is being\n * rendered in.\n *\n * @method setContextId\n * @param {int} id\n */\n setContextId(id) {\n this.contextId = id;\n }\n\n /**\n * Retrieve the saved Moodle context id.\n *\n * @method getContextId\n * @return {int}\n */\n getContextId() {\n return this.contextId;\n }\n\n /**\n * Set the id of the page that the question should be added to\n * when the user clicks the add to quiz link.\n *\n * @method setAddOnPageId\n * @param {int} id\n */\n setAddOnPageId(id) {\n this.addOnPageId = id;\n this.getBody().find(SELECTORS.ADD_ON_PAGE_FORM_ELEMENT).val(id);\n }\n\n /**\n * Returns the saved page id for the question to be added to.\n *\n * @method getAddOnPageId\n * @return {int}\n */\n getAddOnPageId() {\n return this.addOnPageId;\n }\n\n /**\n * Set the category for this form. The category is a comma separated\n * category id and category context id.\n *\n * @method setCategory\n * @param {string} category\n */\n setCategory(category) {\n this.category = category;\n }\n\n /**\n * Returns the saved category.\n *\n * @method getCategory\n * @return {string}\n */\n getCategory() {\n return this.category;\n }\n\n /**\n * Set the return URL for the form.\n *\n * @method setReturnUrl\n * @param {string} url\n */\n setReturnUrl(url) {\n this.returnUrl = url;\n }\n\n /**\n * Returns the return URL for the form.\n *\n * @method getReturnUrl\n * @return {string}\n */\n getReturnUrl() {\n return this.returnUrl;\n }\n\n /**\n * Set the course module id for the form.\n *\n * @method setCMID\n * @param {int} id\n */\n setCMID(id) {\n this.cmid = id;\n }\n\n /**\n * Returns the course module id for the form.\n *\n * @method getCMID\n * @return {int}\n */\n getCMID() {\n return this.cmid;\n }\n\n /**\n * Moves a given form element inside (a child of) a given tab element.\n *\n * Hides the 'legend' (e.g. header) element of the form element because the\n * tab has the name.\n *\n * Moves the submit button into a footer element at the bottom of the form\n * element for styling purposes.\n *\n * @method moveFormElementIntoTab\n * @param {jquery} formElement The form element to move into the tab.\n * @param {jquey} tabElement The tab element for the form element to move into.\n */\n moveFormElementIntoTab(formElement, tabElement) {\n const submitButtons = formElement.find(SELECTORS.SUBMIT_BUTTON_ELEMENT);\n const footer = $('');\n // Hide the header because the tabs show us which part of the form we're\n // looking at.\n formElement.find(SELECTORS.FORM_HEADER).addClass('hidden');\n // Move the element inside a tab.\n formElement.wrap(tabElement);\n // Remove the buttons container element.\n submitButtons.closest(SELECTORS.BUTTON_CONTAINER).remove();\n // Put the button inside a footer.\n submitButtons.appendTo(footer);\n // Add the footer to the end of the category form element.\n footer.appendTo(formElement);\n }\n\n /**\n * Empty the tab content container and move all tabs from the form into the\n * tab container element.\n *\n * @method moveTabsIntoTabContent\n * @param {jquery} form The form element.\n */\n moveTabsIntoTabContent(form) {\n // Empty it to remove the loading icon.\n const tabContent = this.getBody().find(SELECTORS.TAB_CONTENT).empty();\n // Make sure all tabs are inside the tab content element.\n form.find('[role=\"tabpanel\"]').wrapAll(tabContent);\n }\n\n /**\n * Make sure all of the tabs have a cancel button in their fotter to sit along\n * side the submit button.\n *\n * @method moveCancelButtonToTabs\n * @param {jquey} form The form element.\n */\n moveCancelButtonToTabs(form) {\n const cancelButton = form.find(SELECTORS.CANCEL_BUTTON_ELEMENT).addClass('ml-1');\n const tabFooters = form.find('[data-region=\"footer\"]');\n // Remove the buttons container element.\n cancelButton.closest(SELECTORS.BUTTON_CONTAINER).remove();\n cancelButton.clone().appendTo(tabFooters);\n }\n\n /**\n * Load the add random question form in a fragement and perform some transformation\n * on the HTML to convert it into tabs for rendering in the modal.\n *\n * @method loadForm\n * @return {promise} Resolved with form HTML and JS.\n */\n loadForm() {\n return Fragment.loadFragment(\n 'mod_quiz',\n 'add_random_question_form',\n this.getContextId(),\n {\n addonpage: this.getAddOnPageId(),\n cat: this.getCategory(),\n returnurl: this.getReturnUrl(),\n cmid: this.getCMID()\n }\n )\n .then((html, js) =>{\n const form = $(html);\n const existingCategoryFormElement = form.find(SELECTORS.EXISTING_CATEGORY_FORM_ELEMENT);\n const existingCategoryTab = this.getBody().find(SELECTORS.EXISTING_CATEGORY_CONTAINER);\n const newCategoryFormElement = form.find(SELECTORS.NEW_CATEGORY_FORM_ELEMENT);\n const newCategoryTab = this.getBody().find(SELECTORS.NEW_CATEGORY_CONTAINER);\n\n // Transform the form into tabs for better rendering in the modal.\n this.moveFormElementIntoTab(existingCategoryFormElement, existingCategoryTab);\n this.moveFormElementIntoTab(newCategoryFormElement, newCategoryTab);\n this.moveTabsIntoTabContent(form);\n this.moveCancelButtonToTabs(form);\n\n Templates.replaceNode(this.getBody().find(SELECTORS.TAB_CONTENT), form, js);\n return;\n })\n .then(() => {\n // Make sure the form change checker is disabled otherwise it'll stop the user from navigating away from the\n // page once the modal is hidden.\n FormChangeChecker.disableAllChecks();\n return;\n })\n .catch(Notification.exception);\n }\n\n /**\n * Override the modal show function to load the form when this modal is first\n * shown.\n *\n * @method show\n */\n show() {\n super.show(this);\n\n if (!this.loadedForm) {\n this.loadForm();\n this.loadedForm = true;\n }\n }\n}\n\nModalAddRandomQuestion.registerModalType();\n"],"names":["SELECTORS","ModalAddRandomQuestion","Modal","constructor","root","contextId","addOnPageId","category","returnUrl","cmid","loadedForm","setContextId","id","getContextId","this","setAddOnPageId","getBody","find","val","getAddOnPageId","setCategory","getCategory","setReturnUrl","url","getReturnUrl","setCMID","getCMID","moveFormElementIntoTab","formElement","tabElement","submitButtons","footer","addClass","wrap","closest","remove","appendTo","moveTabsIntoTabContent","form","tabContent","empty","wrapAll","moveCancelButtonToTabs","cancelButton","tabFooters","clone","loadForm","Fragment","loadFragment","addonpage","cat","returnurl","then","html","js","existingCategoryFormElement","existingCategoryTab","newCategoryFormElement","newCategoryTab","Templates","replaceNode","FormChangeChecker","disableAllChecks","catch","Notification","exception","show","registerModalType"],"mappings":"2wDA8BMA,sCAC2B,8CAD3BA,yCAE8B,6BAF9BA,iCAGsB,yCAHtBA,oCAIyB,wBAJzBA,sBAKW,8BALXA,mCAMwB,qBANxBA,gCAOqB,uBAPrBA,gCAQqB,sCARrBA,sBASW,SATXA,2BAUgB,eAGDC,+BAA+BC,eAShDC,YAAYC,YACFA,WACDC,UAAY,UACZC,YAAc,UACdC,SAAW,UACXC,UAAY,UACZC,KAAO,UACPC,YAAa,EAUtBC,aAAaC,SACJP,UAAYO,GASrBC,sBACWC,KAAKT,UAUhBU,eAAeH,SACNN,YAAcM,QACdI,UAAUC,KAAKjB,oCAAoCkB,IAAIN,IAShEO,wBACWL,KAAKR,YAUhBc,YAAYb,eACHA,SAAWA,SASpBc,qBACWP,KAAKP,SAShBe,aAAaC,UACJf,UAAYe,IASrBC,sBACWV,KAAKN,UAShBiB,QAAQb,SACCH,KAAOG,GAShBc,iBACWZ,KAAKL,KAgBhBkB,uBAAuBC,YAAaC,kBAC1BC,cAAgBF,YAAYX,KAAKjB,iCACjC+B,QAAS,mBAAE,8DAGjBH,YAAYX,KAAKjB,uBAAuBgC,SAAS,UAEjDJ,YAAYK,KAAKJ,YAEjBC,cAAcI,QAAQlC,4BAA4BmC,SAElDL,cAAcM,SAASL,QAEvBA,OAAOK,SAASR,aAUpBS,uBAAuBC,YAEbC,WAAazB,KAAKE,UAAUC,KAAKjB,uBAAuBwC,QAE9DF,KAAKrB,KAAK,qBAAqBwB,QAAQF,YAU3CG,uBAAuBJ,YACbK,aAAeL,KAAKrB,KAAKjB,iCAAiCgC,SAAS,QACnEY,WAAaN,KAAKrB,KAAK,0BAE7B0B,aAAaT,QAAQlC,4BAA4BmC,SACjDQ,aAAaE,QAAQT,SAASQ,YAUlCE,kBACWC,SAASC,aACZ,WACA,2BACAlC,KAAKD,eACL,CACIoC,UAAWnC,KAAKK,iBAChB+B,IAAKpC,KAAKO,cACV8B,UAAWrC,KAAKU,eAChBf,KAAMK,KAAKY,YAGlB0B,MAAK,CAACC,KAAMC,YACHhB,MAAO,mBAAEe,MACTE,4BAA8BjB,KAAKrB,KAAKjB,0CACxCwD,oBAAsB1C,KAAKE,UAAUC,KAAKjB,uCAC1CyD,uBAAyBnB,KAAKrB,KAAKjB,qCACnC0D,eAAiB5C,KAAKE,UAAUC,KAAKjB,uCAGtC2B,uBAAuB4B,4BAA6BC,0BACpD7B,uBAAuB8B,uBAAwBC,qBAC/CrB,uBAAuBC,WACvBI,uBAAuBJ,MAE5BqB,UAAUC,YAAY9C,KAAKE,UAAUC,KAAKjB,uBAAwBsC,KAAMgB,OAG3EF,MAAK,KAGFS,kBAAkBC,sBAGrBC,MAAMC,aAAaC,WASxBC,aACUA,KAAKpD,MAENA,KAAKJ,kBACDoC,gBACApC,YAAa,mEA5OTT,8BACH,qDADGA,kCAEC,sCA+OtBA,uBAAuBkE"}
\ No newline at end of file
+{"version":3,"file":"modal_add_random_question.min.js","sources":["../src/modal_add_random_question.js"],"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 .\n\n/**\n * Contain the logic for the add random question modal.\n *\n * @module mod_quiz/modal_add_random_question\n * @copyright 2018 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport Modal from 'core/modal';\nimport * as Notification from 'core/notification';\nimport * as Fragment from 'core/fragment';\nimport * as Templates from 'core/templates';\nimport * as FormChangeChecker from 'core_form/changechecker';\nimport {call as fetchMany} from 'core/ajax';\n\nconst SELECTORS = {\n EXISTING_CATEGORY_CONTAINER: '[data-region=\"existing-category-container\"]',\n EXISTING_CATEGORY_TAB: '#id_existingcategoryheader',\n NEW_CATEGORY_CONTAINER: '[data-region=\"new-category-container\"]',\n NEW_CATEGORY_TAB: '#id_newcategoryheader',\n TAB_CONTENT: '[data-region=\"tab-content\"]',\n ADD_ON_PAGE_FORM_ELEMENT: '[name=\"addonpage\"]',\n ADD_RANDOM_BUTTON: 'input[type=\"submit\"][name=\"addrandom\"]',\n ADD_NEW_CATEGORY_BUTTON: 'input[type=\"submit\"][name=\"newcategory\"]',\n SUBMIT_BUTTON_ELEMENT: 'input[type=\"submit\"][name=\"addrandom\"], input[type=\"submit\"][name=\"newcategory\"]',\n FORM_HEADER: 'legend',\n SELECT_NUMBER_TO_ADD: '#menurandomcount',\n NEW_CATEGORY_ELEMENT: '#categoryname',\n PARENT_CATEGORY_ELEMENT: '#parentcategory',\n FILTER_CONDITION_ELEMENT: '[data-filtercondition]',\n FORM_ELEMENT: '#add_random_question_form',\n MESSAGE_INPUT: '[name=\"message\"]',\n};\n\nexport default class ModalAddRandomQuestion extends Modal {\n static TYPE = 'mod_quiz-quiz-add-random-question';\n static TEMPLATE = 'mod_quiz/modal_add_random_question';\n\n /**\n * Constructor for the Modal.\n *\n * @param {object} root The root jQuery element for the modal\n */\n constructor(root) {\n super(root);\n this.contextId = null;\n this.addOnPageId = null;\n this.category = null;\n this.returnUrl = null;\n this.cmid = null;\n this.loadedForm = false;\n }\n\n /**\n * Save the Moodle context id that the question bank is being\n * rendered in.\n *\n * @method setContextId\n * @param {int} id\n */\n setContextId(id) {\n this.contextId = id;\n }\n\n /**\n * Retrieve the saved Moodle context id.\n *\n * @method getContextId\n * @return {int}\n */\n getContextId() {\n return this.contextId;\n }\n\n /**\n * Set the id of the page that the question should be added to\n * when the user clicks the add to quiz link.\n *\n * @method setAddOnPageId\n * @param {int} id\n */\n setAddOnPageId(id) {\n this.addOnPageId = id;\n this.getBody().find(SELECTORS.ADD_ON_PAGE_FORM_ELEMENT).val(id);\n }\n\n /**\n * Returns the saved page id for the question to be added to.\n *\n * @method getAddOnPageId\n * @return {int}\n */\n getAddOnPageId() {\n return this.addOnPageId;\n }\n\n /**\n * Set the category for this form. The category is a comma separated\n * category id and category context id.\n *\n * @method setCategory\n * @param {string} category\n */\n setCategory(category) {\n this.category = category;\n }\n\n /**\n * Returns the saved category.\n *\n * @method getCategory\n * @return {string}\n */\n getCategory() {\n return this.category;\n }\n\n /**\n * Set the return URL for the form.\n *\n * @method setReturnUrl\n * @param {string} url\n */\n setReturnUrl(url) {\n this.returnUrl = url;\n }\n\n /**\n * Returns the return URL for the form.\n *\n * @method getReturnUrl\n * @return {string}\n */\n getReturnUrl() {\n return this.returnUrl;\n }\n\n /**\n * Set the course module id for the form.\n *\n * @method setCMID\n * @param {int} id\n */\n setCMID(id) {\n this.cmid = id;\n }\n\n /**\n * Returns the course module id for the form.\n *\n * @method getCMID\n * @return {int}\n */\n getCMID() {\n return this.cmid;\n }\n\n /**\n * Moves a given form element inside (a child of) a given tab element.\n *\n * Hides the 'legend' (e.g. header) element of the form element because the\n * tab has the name.\n *\n * Moves the submit button into a footer element at the bottom of the form\n * element for styling purposes.\n *\n * @method moveContentIntoTab\n * @param {jquery} tabContent The form element to move into the tab.\n * @param {jquey} tabElement The tab element for the form element to move into.\n */\n moveContentIntoTab(tabContent, tabElement) {\n // Hide the header because the tabs show us which part of the form we're\n // looking at.\n tabContent.find(SELECTORS.FORM_HEADER).addClass('hidden');\n // Move the element inside a tab.\n tabContent.wrap(tabElement);\n }\n\n /**\n * Empty the tab content container and move all tabs from the form into the\n * tab container element.\n *\n * @method moveTabsIntoTabContent\n * @param {jquery} form The form element.\n */\n moveTabsIntoTabContent(form) {\n // Empty it to remove the loading icon.\n const tabContent = this.getBody().find(SELECTORS.TAB_CONTENT).empty();\n // Make sure all tabs are inside the tab content element.\n form.find('[role=\"tabpanel\"]').wrapAll(tabContent);\n }\n\n /**\n * Make sure all of the tabs have a cancel button in their fotter to sit along\n * side the submit button.\n *\n * @method moveCancelButtonToTabs\n * @param {jquey} form The form element.\n */\n moveCancelButtonToTabs(form) {\n const cancelButton = form.find(SELECTORS.CANCEL_BUTTON_ELEMENT).addClass('ml-1');\n const tabFooters = form.find('[data-region=\"footer\"]');\n // Remove the buttons container element.\n cancelButton.closest(SELECTORS.BUTTON_CONTAINER).remove();\n cancelButton.clone().appendTo(tabFooters);\n }\n\n /**\n * Load the add random question form in a fragement and perform some transformation\n * on the HTML to convert it into tabs for rendering in the modal.\n *\n * @method loadForm\n * @return {promise} Resolved with form HTML and JS.\n */\n loadForm() {\n const cmid = this.getCMID();\n const cat = this.getCategory();\n const addonpage = this.getAddOnPageId();\n const returnurl = this.getReturnUrl();\n\n return Fragment.loadFragment(\n 'mod_quiz',\n 'add_random_question_form',\n this.getContextId(),\n {\n addonpage,\n cat,\n returnurl,\n cmid,\n }\n )\n .then((html, js) =>{\n const form = $(html);\n const existingCategoryTabContent = form.find(SELECTORS.EXISTING_CATEGORY_TAB);\n const existingCategoryTab = this.getBody().find(SELECTORS.EXISTING_CATEGORY_CONTAINER);\n const newCategoryTabContent = form.find(SELECTORS.NEW_CATEGORY_TAB);\n const newCategoryTab = this.getBody().find(SELECTORS.NEW_CATEGORY_CONTAINER);\n\n // Transform the form into tabs for better rendering in the modal.\n this.moveContentIntoTab(existingCategoryTabContent, existingCategoryTab);\n this.moveContentIntoTab(newCategoryTabContent, newCategoryTab);\n this.moveTabsIntoTabContent(form);\n\n Templates.replaceNode(this.getBody().find(SELECTORS.TAB_CONTENT), form, js);\n return;\n })\n .then(() => {\n // Make sure the form change checker is disabled otherwise it'll stop the user from navigating away from the\n // page once the modal is hidden.\n FormChangeChecker.disableAllChecks();\n\n // Add question to quiz.\n this.getBody()[0].addEventListener('click', (e) => {\n const button = e.target.closest(SELECTORS.SUBMIT_BUTTON_ELEMENT);\n if (!button) {\n return;\n }\n e.preventDefault();\n\n const randomcount = document.querySelector(SELECTORS.SELECT_NUMBER_TO_ADD).value;\n const filtercondition = document.querySelector(SELECTORS.FILTER_CONDITION_ELEMENT).dataset?.filtercondition;\n\n // Add Random questions if the add random button was clicked.\n const addRandomButton = e.target.closest(SELECTORS.ADD_RANDOM_BUTTON);\n if (addRandomButton) {\n this.addQuestions(cmid, addonpage, randomcount, filtercondition, '', '');\n return;\n }\n // Add new category if the add category button was clicked.\n const addCategoryButton = e.target.closest(SELECTORS.ADD_NEW_CATEGORY_BUTTON);\n if (addCategoryButton) {\n this.addQuestions(\n cmid,\n addonpage,\n randomcount,\n filtercondition,\n document.querySelector(SELECTORS.NEW_CATEGORY_ELEMENT).value,\n document.querySelector(SELECTORS.PARENT_CATEGORY_ELEMENT).value\n );\n return;\n }\n });\n })\n .catch(Notification.exception);\n }\n\n /**\n * Call web service function to add random questions\n *\n * @param {number} cmid course module id\n * @param {number} addonpage the page where random questions will be added to\n * @param {number} randomcount Number of random questions\n * @param {string} filtercondition Filter condition\n * @param {string} newcategory add new category\n * @param {string} parentcategory parent category of new category\n */\n async addQuestions(\n cmid,\n addonpage,\n randomcount,\n filtercondition,\n newcategory,\n parentcategory\n ) {\n const call = {\n methodname: 'mod_quiz_add_random_questions',\n args: {\n cmid,\n addonpage,\n randomcount,\n filtercondition,\n newcategory,\n parentcategory,\n }\n };\n try {\n const response = await fetchMany([call])[0];\n const form = document.querySelector(SELECTORS.FORM_ELEMENT);\n const messageInput = form.querySelector(SELECTORS.MESSAGE_INPUT);\n messageInput.value = response.message;\n form.submit();\n } catch (e) {\n Notification.exception(e);\n }\n }\n\n /**\n * Override the modal show function to load the form when this modal is first\n * shown.\n *\n * @method show\n */\n show() {\n super.show(this);\n\n if (!this.loadedForm) {\n this.loadForm(window.location.search);\n this.loadedForm = true;\n }\n }\n}\n\nModalAddRandomQuestion.registerModalType();\n"],"names":["SELECTORS","EXISTING_CATEGORY_CONTAINER","EXISTING_CATEGORY_TAB","NEW_CATEGORY_CONTAINER","NEW_CATEGORY_TAB","TAB_CONTENT","ADD_ON_PAGE_FORM_ELEMENT","ADD_RANDOM_BUTTON","ADD_NEW_CATEGORY_BUTTON","SUBMIT_BUTTON_ELEMENT","FORM_HEADER","SELECT_NUMBER_TO_ADD","NEW_CATEGORY_ELEMENT","PARENT_CATEGORY_ELEMENT","FILTER_CONDITION_ELEMENT","FORM_ELEMENT","MESSAGE_INPUT","ModalAddRandomQuestion","Modal","constructor","root","contextId","addOnPageId","category","returnUrl","cmid","loadedForm","setContextId","id","getContextId","this","setAddOnPageId","getBody","find","val","getAddOnPageId","setCategory","getCategory","setReturnUrl","url","getReturnUrl","setCMID","getCMID","moveContentIntoTab","tabContent","tabElement","addClass","wrap","moveTabsIntoTabContent","form","empty","wrapAll","moveCancelButtonToTabs","cancelButton","CANCEL_BUTTON_ELEMENT","tabFooters","closest","BUTTON_CONTAINER","remove","clone","appendTo","loadForm","cat","addonpage","returnurl","Fragment","loadFragment","then","html","js","existingCategoryTabContent","existingCategoryTab","newCategoryTabContent","newCategoryTab","Templates","replaceNode","FormChangeChecker","disableAllChecks","addEventListener","e","target","preventDefault","randomcount","document","querySelector","value","filtercondition","dataset","_document$querySelect","addQuestions","catch","Notification","exception","newcategory","parentcategory","call","methodname","args","response","message","submit","show","window","location","search","registerModalType"],"mappings":"6xDA+BMA,UAAY,CACdC,4BAA6B,8CAC7BC,sBAAuB,6BACvBC,uBAAwB,yCACxBC,iBAAkB,wBAClBC,YAAa,8BACbC,yBAA0B,qBAC1BC,kBAAmB,yCACnBC,wBAAyB,2CACzBC,sBAAuB,mFACvBC,YAAa,SACbC,qBAAsB,mBACtBC,qBAAsB,gBACtBC,wBAAyB,kBACzBC,yBAA0B,yBAC1BC,aAAc,4BACdC,cAAe,0BAGEC,+BAA+BC,eAShDC,YAAYC,YACFA,WACDC,UAAY,UACZC,YAAc,UACdC,SAAW,UACXC,UAAY,UACZC,KAAO,UACPC,YAAa,EAUtBC,aAAaC,SACJP,UAAYO,GASrBC,sBACWC,KAAKT,UAUhBU,eAAeH,SACNN,YAAcM,QACdI,UAAUC,KAAKjC,UAAUM,0BAA0B4B,IAAIN,IAShEO,wBACWL,KAAKR,YAUhBc,YAAYb,eACHA,SAAWA,SASpBc,qBACWP,KAAKP,SAShBe,aAAaC,UACJf,UAAYe,IASrBC,sBACWV,KAAKN,UAShBiB,QAAQb,SACCH,KAAOG,GAShBc,iBACWZ,KAAKL,KAgBhBkB,mBAAmBC,WAAYC,YAG3BD,WAAWX,KAAKjC,UAAUU,aAAaoC,SAAS,UAEhDF,WAAWG,KAAKF,YAUpBG,uBAAuBC,YAEbL,WAAad,KAAKE,UAAUC,KAAKjC,UAAUK,aAAa6C,QAE9DD,KAAKhB,KAAK,qBAAqBkB,QAAQP,YAU3CQ,uBAAuBH,YACbI,aAAeJ,KAAKhB,KAAKjC,UAAUsD,uBAAuBR,SAAS,QACnES,WAAaN,KAAKhB,KAAK,0BAE7BoB,aAAaG,QAAQxD,UAAUyD,kBAAkBC,SACjDL,aAAaM,QAAQC,SAASL,YAUlCM,iBACUpC,KAAOK,KAAKY,UACZoB,IAAMhC,KAAKO,cACX0B,UAAYjC,KAAKK,iBACjB6B,UAAYlC,KAAKU,sBAEhByB,SAASC,aACZ,WACA,2BACApC,KAAKD,eACL,CACIkC,UAAAA,UACAD,IAAAA,IACAE,UAAAA,UACAvC,KAAAA,OAGP0C,MAAK,CAACC,KAAMC,YACHpB,MAAO,mBAAEmB,MACTE,2BAA6BrB,KAAKhB,KAAKjC,UAAUE,uBACjDqE,oBAAsBzC,KAAKE,UAAUC,KAAKjC,UAAUC,6BACpDuE,sBAAwBvB,KAAKhB,KAAKjC,UAAUI,kBAC5CqE,eAAiB3C,KAAKE,UAAUC,KAAKjC,UAAUG,6BAGhDwC,mBAAmB2B,2BAA4BC,0BAC/C5B,mBAAmB6B,sBAAuBC,qBAC1CzB,uBAAuBC,MAE5ByB,UAAUC,YAAY7C,KAAKE,UAAUC,KAAKjC,UAAUK,aAAc4C,KAAMoB,OAG3EF,MAAK,KAGFS,kBAAkBC,wBAGb7C,UAAU,GAAG8C,iBAAiB,SAAUC,kCAC1BA,EAAEC,OAAOxB,QAAQxD,UAAUS,8BAI1CsE,EAAEE,uBAEIC,YAAcC,SAASC,cAAcpF,UAAUW,sBAAsB0E,MACrEC,8CAAkBH,SAASC,cAAcpF,UAAUc,0BAA0ByE,gDAA3DC,sBAAoEF,mBAGpEP,EAAEC,OAAOxB,QAAQxD,UAAUO,oCAE1CkF,aAAahE,KAAMsC,UAAWmB,YAAaI,gBAAiB,GAAI,IAI/CP,EAAEC,OAAOxB,QAAQxD,UAAUQ,+BAE5CiF,aACDhE,KACAsC,UACAmB,YACAI,gBACAH,SAASC,cAAcpF,UAAUY,sBAAsByE,MACvDF,SAASC,cAAcpF,UAAUa,yBAAyBwE,aAMzEK,MAAMC,aAAaC,8BAcpBnE,KACAsC,UACAmB,YACAI,gBACAO,YACAC,sBAEMC,KAAO,CACTC,WAAY,gCACZC,KAAM,CACFxE,KAAAA,KACAsC,UAAAA,UACAmB,YAAAA,YACAI,gBAAAA,gBACAO,YAAAA,YACAC,eAAAA,2BAIEI,eAAiB,cAAU,CAACH,OAAO,GACnC9C,KAAOkC,SAASC,cAAcpF,UAAUe,cACzBkC,KAAKmC,cAAcpF,UAAUgB,eACrCqE,MAAQa,SAASC,QAC9BlD,KAAKmD,SACP,MAAOrB,GACLY,aAAaC,UAAUb,IAU/BsB,aACUA,KAAKvE,MAENA,KAAKJ,kBACDmC,SAASyC,OAAOC,SAASC,aACzB9E,YAAa,mEA/STT,8BACH,qDADGA,kCAEC,sCAkTtBA,uBAAuBwF"}
\ No newline at end of file
diff --git a/mod/quiz/amd/build/modal_quiz_question_bank.min.js b/mod/quiz/amd/build/modal_quiz_question_bank.min.js
index 9b8b9a7c194..d9285224524 100644
--- a/mod/quiz/amd/build/modal_quiz_question_bank.min.js
+++ b/mod/quiz/amd/build/modal_quiz_question_bank.min.js
@@ -1,3 +1,3 @@
-define("mod_quiz/modal_quiz_question_bank",["exports","jquery","core/modal","core/fragment","core_form/changechecker","core/modal_events"],(function(_exports,_jquery,_modal,Fragment,FormChangeChecker,ModalEvents){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_jquery=_interopRequireDefault(_jquery),_modal=_interopRequireDefault(_modal),Fragment=_interopRequireWildcard(Fragment),FormChangeChecker=_interopRequireWildcard(FormChangeChecker),ModalEvents=_interopRequireWildcard(ModalEvents);const SELECTORS_ADD_TO_QUIZ_CONTAINER="td.addtoquizaction",SELECTORS_ANCHOR="a[href]",SELECTORS_PREVIEW_CONTAINER="td.previewaction",SELECTORS_SEARCH_OPTIONS="#advancedsearch",SELECTORS_DISPLAY_OPTIONS="#displayoptions",SELECTORS_ADD_QUESTIONS_FORM="form#questionsubmit";class ModalQuizQuestionBank extends _modal.default{constructor(root){super(root),this.contextId=null,this.addOnPageId=null}setContextId(id){this.contextId=id}getContextId(){return this.contextId}setAddOnPageId(id){this.addOnPageId=id}getAddOnPageId(){return this.addOnPageId}show(){return this.reloadBodyContent(window.location.search),super.show(this)}reloadBodyContent(querystring){this.setBody(Fragment.loadFragment("mod_quiz","quiz_question_bank",this.getContextId(),{querystring:querystring}))}handleAddToQuizEvent(e,anchorElement){const href=new URL(anchorElement.attr("href"));href.searchParams.set("addonpage",this.getAddOnPageId()),anchorElement.attr("href",href)}handlePreviewContainerEvent(e,anchorElement){window.openpopup(e,{url:anchorElement.attr("href"),name:"questionpreview",options:["height=600","width=800","top=0","left=0","menubar=0","location=0","scrollbars","resizable","toolbar","status","directories=0","fullscreen=0","dependent"].join(",")})}handleDisplayOptionFormEvent(e){e.stopPropagation(),e.preventDefault();const queryString="?"+(0,_jquery.default)(e.target).closest(SELECTORS_DISPLAY_OPTIONS).serialize();this.reloadBodyContent(queryString)}registerDisplayOptionListeners(){this.getModal().on("change",SELECTORS_DISPLAY_OPTIONS,(e=>{(0,_jquery.default)(e.target).attr("aria-autocomplete")||this.handleDisplayOptionFormEvent(e)})),this.getModal().on("submit",SELECTORS_DISPLAY_OPTIONS,(e=>{this.handleDisplayOptionFormEvent(e)}))}registerEventListeners(){super.registerEventListeners(this),this.registerDisplayOptionListeners(),this.getModal().on("submit",SELECTORS_ADD_QUESTIONS_FORM,(e=>{const formElement=(0,_jquery.default)(e.currentTarget);(0,_jquery.default)(" ").attr("type","hidden").attr("name","addonpage").attr("value",this.getAddOnPageId()).appendTo(formElement)})),this.getModal().on("click",SELECTORS_ANCHOR,(e=>{const anchorElement=(0,_jquery.default)(e.currentTarget);anchorElement.closest(SELECTORS_ADD_TO_QUIZ_CONTAINER).length?this.handleAddToQuizEvent(e,anchorElement):anchorElement.closest(SELECTORS_PREVIEW_CONTAINER).length?this.handlePreviewContainerEvent(e,anchorElement):anchorElement.closest(SELECTORS_SEARCH_OPTIONS).length||(e.preventDefault(),this.reloadBodyContent(anchorElement.prop("search")))})),this.getRoot().on(ModalEvents.bodyRendered,(()=>{FormChangeChecker.disableAllChecks()}))}}var obj,key,value;return _exports.default=ModalQuizQuestionBank,value="mod_quiz-quiz-question-bank",(key="TYPE")in(obj=ModalQuizQuestionBank)?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,ModalQuizQuestionBank.registerModalType(),_exports.default}));
+define("mod_quiz/modal_quiz_question_bank",["exports","jquery","core/modal","core/fragment","core_form/changechecker","core/modal_events"],(function(_exports,_jquery,_modal,Fragment,FormChangeChecker,ModalEvents){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_jquery=_interopRequireDefault(_jquery),_modal=_interopRequireDefault(_modal),Fragment=_interopRequireWildcard(Fragment),FormChangeChecker=_interopRequireWildcard(FormChangeChecker),ModalEvents=_interopRequireWildcard(ModalEvents);const SELECTORS_ADD_TO_QUIZ_CONTAINER="td.addtoquizaction",SELECTORS_ANCHOR="a[href]",SELECTORS_PREVIEW_CONTAINER="td.previewaction",SELECTORS_ADD_QUESTIONS_FORM="form#questionsubmit",SELECTORS_SORTERS=".sorters";class ModalQuizQuestionBank extends _modal.default{constructor(root){super(root),this.contextId=null,this.addOnPageId=null}setContextId(id){this.contextId=id}getContextId(){return this.contextId}setAddOnPageId(id){this.addOnPageId=id}getAddOnPageId(){return this.addOnPageId}show(){return this.reloadBodyContent(window.location.search),super.show(this)}reloadBodyContent(querystring){this.setBody(Fragment.loadFragment("mod_quiz","quiz_question_bank",this.getContextId(),{querystring:querystring}))}handleAddToQuizEvent(e,anchorElement){const href=new URL(anchorElement.attr("href"));href.searchParams.set("addonpage",this.getAddOnPageId()),anchorElement.attr("href",href)}handlePreviewContainerEvent(e,anchorElement){window.openpopup(e,{url:anchorElement.attr("href"),name:"questionpreview",options:["height=600","width=800","top=0","left=0","menubar=0","location=0","scrollbars","resizable","toolbar","status","directories=0","fullscreen=0","dependent"].join(",")})}registerEventListeners(){super.registerEventListeners(this),this.getModal().on("submit",SELECTORS_ADD_QUESTIONS_FORM,(e=>{const formElement=(0,_jquery.default)(e.currentTarget);(0,_jquery.default)(" ").attr("type","hidden").attr("name","addonpage").attr("value",this.getAddOnPageId()).appendTo(formElement)})),this.getModal().on("click",SELECTORS_ANCHOR,(e=>{const anchorElement=(0,_jquery.default)(e.currentTarget);anchorElement.closest(SELECTORS_ADD_TO_QUIZ_CONTAINER).length?this.handleAddToQuizEvent(e,anchorElement):anchorElement.closest(SELECTORS_PREVIEW_CONTAINER).length?this.handlePreviewContainerEvent(e,anchorElement):anchorElement.closest(SELECTORS_SORTERS).length||(e.preventDefault(),this.reloadBodyContent(anchorElement.prop("search")))})),this.getRoot().on(ModalEvents.bodyRendered,(()=>{FormChangeChecker.disableAllChecks()}))}}var obj,key,value;return _exports.default=ModalQuizQuestionBank,value="mod_quiz-quiz-question-bank",(key="TYPE")in(obj=ModalQuizQuestionBank)?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,ModalQuizQuestionBank.registerModalType(),_exports.default}));
//# sourceMappingURL=modal_quiz_question_bank.min.js.map
\ No newline at end of file
diff --git a/mod/quiz/amd/build/modal_quiz_question_bank.min.js.map b/mod/quiz/amd/build/modal_quiz_question_bank.min.js.map
index 46f0ef7bff8..9eb5605e017 100644
--- a/mod/quiz/amd/build/modal_quiz_question_bank.min.js.map
+++ b/mod/quiz/amd/build/modal_quiz_question_bank.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"modal_quiz_question_bank.min.js","sources":["../src/modal_quiz_question_bank.js"],"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 .\n\n/**\n * Contain the logic for the question bank modal.\n *\n * @module mod_quiz/modal_quiz_question_bank\n * @copyright 2018 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport Modal from 'core/modal';\nimport * as Fragment from 'core/fragment';\nimport * as FormChangeChecker from 'core_form/changechecker';\nimport * as ModalEvents from 'core/modal_events';\n\nconst SELECTORS = {\n ADD_TO_QUIZ_CONTAINER: 'td.addtoquizaction',\n ANCHOR: 'a[href]',\n PREVIEW_CONTAINER: 'td.previewaction',\n SEARCH_OPTIONS: '#advancedsearch',\n DISPLAY_OPTIONS: '#displayoptions',\n ADD_QUESTIONS_FORM: 'form#questionsubmit',\n};\n\nexport default class ModalQuizQuestionBank extends Modal {\n static TYPE = 'mod_quiz-quiz-question-bank';\n\n /**\n * Constructor for the Modal.\n *\n * @param {object} root The root jQuery element for the modal\n */\n constructor(root) {\n super(root);\n\n this.contextId = null;\n this.addOnPageId = null;\n }\n\n /**\n * Save the Moodle context id that the question bank is being\n * rendered in.\n *\n * @method setContextId\n * @param {int} id\n */\n setContextId(id) {\n this.contextId = id;\n }\n\n /**\n * Retrieve the saved Moodle context id.\n *\n * @method getContextId\n * @return {int}\n */\n getContextId() {\n return this.contextId;\n }\n\n /**\n * Set the id of the page that the question should be added to\n * when the user clicks the add to quiz link.\n *\n * @method setAddOnPageId\n * @param {int} id\n */\n setAddOnPageId(id) {\n this.addOnPageId = id;\n }\n\n /**\n * Returns the saved page id for the question to be added it.\n *\n * @method getAddOnPageId\n * @return {int}\n */\n getAddOnPageId() {\n return this.addOnPageId;\n }\n\n /**\n * Override the parent show function.\n *\n * Reload the body contents when the modal is shown. The current\n * window URL is used to inform the new content that should be\n * displayed.\n *\n * @method show\n * @return {void}\n */\n show() {\n this.reloadBodyContent(window.location.search);\n return super.show(this);\n }\n\n /**\n * Replaces the current body contents with a new version of the question\n * bank.\n *\n * The contents of the question bank are generated using the provided\n * query string.\n *\n * @method reloadBodyContent\n * @param {string} querystring URL encoded string.\n */\n reloadBodyContent(querystring) {\n // Load the question bank fragment to be displayed in the modal.\n this.setBody(Fragment.loadFragment(\n 'mod_quiz',\n 'quiz_question_bank',\n this.getContextId(),\n {\n querystring,\n }\n ));\n }\n\n /**\n * Update the URL of the anchor element that the user clicked on to make\n * sure that the question is added to the correct page.\n *\n * @method handleAddToQuizEvent\n * @param {event} e A JavaScript event\n * @param {object} anchorElement The anchor element that was triggered\n */\n handleAddToQuizEvent(e, anchorElement) {\n // If the user clicks the plus icon to add the question to the page\n // directly then we need to intercept the click in order to adjust the\n // href and include the correct add on page id before the page is\n // redirected.\n const href = new URL(anchorElement.attr('href'));\n href.searchParams.set('addonpage', this.getAddOnPageId());\n anchorElement.attr('href', href);\n }\n\n /**\n * Open a popup window to show the preview of the question.\n *\n * @method handlePreviewContainerEvent\n * @param {event} e A JavaScript event\n * @param {object} anchorElement The anchor element that was triggered\n */\n handlePreviewContainerEvent(e, anchorElement) {\n const popupOptions = [\n 'height=600',\n 'width=800',\n 'top=0',\n 'left=0',\n 'menubar=0',\n 'location=0',\n 'scrollbars',\n 'resizable',\n 'toolbar',\n 'status',\n 'directories=0',\n 'fullscreen=0',\n 'dependent',\n ];\n window.openpopup(e, {\n url: anchorElement.attr('href'),\n name: 'questionpreview',\n options: popupOptions.join(',')\n });\n }\n\n /**\n * Reload the modal body with the new display options the user has selected.\n *\n * A query string is built using the form elements to be used to generate the\n * new body content.\n *\n * @method handleDisplayOptionFormEvent\n * @param {event} e A JavaScript event\n */\n handleDisplayOptionFormEvent(e) {\n // Stop propagation to prevent other wild event handlers\n // from submitting the form on change.\n e.stopPropagation();\n e.preventDefault();\n\n const form = $(e.target).closest(SELECTORS.DISPLAY_OPTIONS);\n const queryString = '?' + form.serialize();\n this.reloadBodyContent(queryString);\n }\n\n /**\n * Listen for changes to the display options form.\n *\n * This handles the user changing:\n * - The quiz category select box\n * - The tags to filter by\n * - Show/hide questions from sub categories\n * - Show/hide old questions\n *\n * @method registerDisplayOptionListeners\n */\n registerDisplayOptionListeners() {\n // Listen for changes to the display options form.\n this.getModal().on('change', SELECTORS.DISPLAY_OPTIONS, (e) => {\n // Get the element that was changed.\n const modifiedElement = $(e.target);\n if (modifiedElement.attr('aria-autocomplete')) {\n // If the element that was change is the autocomplete\n // input then we should ignore it because that is for\n // display purposes only.\n return;\n }\n\n this.handleDisplayOptionFormEvent(e);\n });\n\n // Listen for the display options form submission because the tags\n // filter will submit the form when it is changed.\n this.getModal().on('submit', SELECTORS.DISPLAY_OPTIONS, (e) => {\n this.handleDisplayOptionFormEvent(e);\n });\n }\n\n /**\n * Set up all of the event handling for the modal.\n *\n * @method registerEventListeners\n */\n registerEventListeners() {\n // Apply parent event listeners.\n super.registerEventListeners(this);\n\n // Set up the event handlers for all of the display options.\n this.registerDisplayOptionListeners();\n\n this.getModal().on('submit', SELECTORS.ADD_QUESTIONS_FORM, (e) => {\n // If the user clicks on the \"Add selected questions to the quiz\" button to add some questions to the page\n // then we need to intercept the submit in order to include the correct \"add on page id\" before the form is\n // submitted.\n const formElement = $(e.currentTarget);\n\n $(' ').attr('type', 'hidden')\n .attr('name', \"addonpage\")\n .attr('value', this.getAddOnPageId())\n .appendTo(formElement);\n });\n\n this.getModal().on('click', SELECTORS.ANCHOR, (e) => {\n const anchorElement = $(e.currentTarget);\n\n // If the anchor element was the add to quiz link.\n if (anchorElement.closest(SELECTORS.ADD_TO_QUIZ_CONTAINER).length) {\n this.handleAddToQuizEvent(e, anchorElement);\n return;\n }\n\n // If the anchor element was a preview question link.\n if (anchorElement.closest(SELECTORS.PREVIEW_CONTAINER).length) {\n this.handlePreviewContainerEvent(e, anchorElement);\n return;\n }\n\n // Click on expand/collaspse search-options. Has its own handler.\n // We should not interfere.\n if (anchorElement.closest(SELECTORS.SEARCH_OPTIONS).length) {\n return;\n }\n\n // Anything else means reload the pop-up contents.\n e.preventDefault();\n this.reloadBodyContent(anchorElement.prop('search'));\n });\n\n // Disable the form change checker when the body is rendered.\n this.getRoot().on(ModalEvents.bodyRendered, () => {\n // Make sure the form change checker is disabled otherwise it'll stop the user from navigating away from the\n // page once the modal is hidden.\n FormChangeChecker.disableAllChecks();\n });\n }\n}\n\nModalQuizQuestionBank.registerModalType();\n"],"names":["SELECTORS","ModalQuizQuestionBank","Modal","constructor","root","contextId","addOnPageId","setContextId","id","getContextId","this","setAddOnPageId","getAddOnPageId","show","reloadBodyContent","window","location","search","super","querystring","setBody","Fragment","loadFragment","handleAddToQuizEvent","e","anchorElement","href","URL","attr","searchParams","set","handlePreviewContainerEvent","openpopup","url","name","options","join","handleDisplayOptionFormEvent","stopPropagation","preventDefault","queryString","target","closest","serialize","registerDisplayOptionListeners","getModal","on","registerEventListeners","formElement","currentTarget","appendTo","length","prop","getRoot","ModalEvents","bodyRendered","FormChangeChecker","disableAllChecks","registerModalType"],"mappings":"2hDA6BMA,gCACqB,qBADrBA,iBAEM,UAFNA,4BAGiB,mBAHjBA,yBAIc,kBAJdA,0BAKe,kBALfA,6BAMkB,4BAGHC,8BAA8BC,eAQ/CC,YAAYC,YACFA,WAEDC,UAAY,UACZC,YAAc,KAUvBC,aAAaC,SACJH,UAAYG,GASrBC,sBACWC,KAAKL,UAUhBM,eAAeH,SACNF,YAAcE,GASvBI,wBACWF,KAAKJ,YAahBO,mBACSC,kBAAkBC,OAAOC,SAASC,QAChCC,MAAML,KAAKH,MAatBI,kBAAkBK,kBAETC,QAAQC,SAASC,aAClB,WACA,qBACAZ,KAAKD,eACL,CACIU,YAAAA,eAaZI,qBAAqBC,EAAGC,qBAKdC,KAAO,IAAIC,IAAIF,cAAcG,KAAK,SACxCF,KAAKG,aAAaC,IAAI,YAAapB,KAAKE,kBACxCa,cAAcG,KAAK,OAAQF,MAU/BK,4BAA4BP,EAAGC,eAgB3BV,OAAOiB,UAAUR,EAAG,CAChBS,IAAKR,cAAcG,KAAK,QACxBM,KAAM,kBACNC,QAlBiB,CACjB,aACA,YACA,QACA,SACA,YACA,aACA,aACA,YACA,UACA,SACA,gBACA,eACA,aAKsBC,KAAK,OAanCC,6BAA6Bb,GAGzBA,EAAEc,kBACFd,EAAEe,uBAGIC,YAAc,KADP,mBAAEhB,EAAEiB,QAAQC,QAAQ1C,2BACF2C,iBAC1B7B,kBAAkB0B,aAc3BI,sCAESC,WAAWC,GAAG,SAAU9C,2BAA4BwB,KAE7B,mBAAEA,EAAEiB,QACRb,KAAK,2BAOpBS,6BAA6Bb,WAKjCqB,WAAWC,GAAG,SAAU9C,2BAA4BwB,SAChDa,6BAA6Bb,MAS1CuB,+BAEUA,uBAAuBrC,WAGxBkC,sCAEAC,WAAWC,GAAG,SAAU9C,8BAA+BwB,UAIlDwB,aAAc,mBAAExB,EAAEyB,mCAEtB,aAAarB,KAAK,OAAQ,UACvBA,KAAK,OAAQ,aACbA,KAAK,QAASlB,KAAKE,kBACnBsC,SAASF,qBAGbH,WAAWC,GAAG,QAAS9C,kBAAmBwB,UACrCC,eAAgB,mBAAED,EAAEyB,eAGtBxB,cAAciB,QAAQ1C,iCAAiCmD,YAClD5B,qBAAqBC,EAAGC,eAK7BA,cAAciB,QAAQ1C,6BAA6BmD,YAC9CpB,4BAA4BP,EAAGC,eAMpCA,cAAciB,QAAQ1C,0BAA0BmD,SAKpD3B,EAAEe,sBACGzB,kBAAkBW,cAAc2B,KAAK,oBAIzCC,UAAUP,GAAGQ,YAAYC,cAAc,KAGxCC,kBAAkBC,6FAxPZ,iDADGxD,6HA8PrBA,sBAAsByD"}
\ No newline at end of file
+{"version":3,"file":"modal_quiz_question_bank.min.js","sources":["../src/modal_quiz_question_bank.js"],"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 .\n\n/**\n * Contain the logic for the question bank modal.\n *\n * @module mod_quiz/modal_quiz_question_bank\n * @copyright 2018 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport Modal from 'core/modal';\nimport * as Fragment from 'core/fragment';\nimport * as FormChangeChecker from 'core_form/changechecker';\nimport * as ModalEvents from 'core/modal_events';\n\nconst SELECTORS = {\n ADD_TO_QUIZ_CONTAINER: 'td.addtoquizaction',\n ANCHOR: 'a[href]',\n PREVIEW_CONTAINER: 'td.previewaction',\n ADD_QUESTIONS_FORM: 'form#questionsubmit',\n SORTERS: '.sorters',\n};\n\nexport default class ModalQuizQuestionBank extends Modal {\n static TYPE = 'mod_quiz-quiz-question-bank';\n\n /**\n * Constructor for the Modal.\n *\n * @param {object} root The root jQuery element for the modal\n */\n constructor(root) {\n super(root);\n\n this.contextId = null;\n this.addOnPageId = null;\n }\n\n /**\n * Save the Moodle context id that the question bank is being\n * rendered in.\n *\n * @method setContextId\n * @param {int} id\n */\n setContextId(id) {\n this.contextId = id;\n }\n\n /**\n * Retrieve the saved Moodle context id.\n *\n * @method getContextId\n * @return {int}\n */\n getContextId() {\n return this.contextId;\n }\n\n /**\n * Set the id of the page that the question should be added to\n * when the user clicks the add to quiz link.\n *\n * @method setAddOnPageId\n * @param {int} id\n */\n setAddOnPageId(id) {\n this.addOnPageId = id;\n }\n\n /**\n * Returns the saved page id for the question to be added it.\n *\n * @method getAddOnPageId\n * @return {int}\n */\n getAddOnPageId() {\n return this.addOnPageId;\n }\n\n /**\n * Override the parent show function.\n *\n * Reload the body contents when the modal is shown. The current\n * window URL is used to inform the new content that should be\n * displayed.\n *\n * @method show\n * @return {void}\n */\n show() {\n this.reloadBodyContent(window.location.search);\n return super.show(this);\n }\n\n /**\n * Replaces the current body contents with a new version of the question\n * bank.\n *\n * The contents of the question bank are generated using the provided\n * query string.\n *\n * @method reloadBodyContent\n * @param {string} querystring URL encoded string.\n */\n reloadBodyContent(querystring) {\n // Load the question bank fragment to be displayed in the modal.\n this.setBody(Fragment.loadFragment(\n 'mod_quiz',\n 'quiz_question_bank',\n this.getContextId(),\n {\n querystring,\n }\n ));\n }\n\n /**\n * Update the URL of the anchor element that the user clicked on to make\n * sure that the question is added to the correct page.\n *\n * @method handleAddToQuizEvent\n * @param {event} e A JavaScript event\n * @param {object} anchorElement The anchor element that was triggered\n */\n handleAddToQuizEvent(e, anchorElement) {\n // If the user clicks the plus icon to add the question to the page\n // directly then we need to intercept the click in order to adjust the\n // href and include the correct add on page id before the page is\n // redirected.\n const href = new URL(anchorElement.attr('href'));\n href.searchParams.set('addonpage', this.getAddOnPageId());\n anchorElement.attr('href', href);\n }\n\n /**\n * Open a popup window to show the preview of the question.\n *\n * @method handlePreviewContainerEvent\n * @param {event} e A JavaScript event\n * @param {object} anchorElement The anchor element that was triggered\n */\n handlePreviewContainerEvent(e, anchorElement) {\n const popupOptions = [\n 'height=600',\n 'width=800',\n 'top=0',\n 'left=0',\n 'menubar=0',\n 'location=0',\n 'scrollbars',\n 'resizable',\n 'toolbar',\n 'status',\n 'directories=0',\n 'fullscreen=0',\n 'dependent',\n ];\n window.openpopup(e, {\n url: anchorElement.attr('href'),\n name: 'questionpreview',\n options: popupOptions.join(',')\n });\n }\n\n /**\n * Set up all of the event handling for the modal.\n *\n * @method registerEventListeners\n */\n registerEventListeners() {\n // Apply parent event listeners.\n super.registerEventListeners(this);\n\n this.getModal().on('submit', SELECTORS.ADD_QUESTIONS_FORM, (e) => {\n // If the user clicks on the \"Add selected questions to the quiz\" button to add some questions to the page\n // then we need to intercept the submit in order to include the correct \"add on page id\" before the form is\n // submitted.\n const formElement = $(e.currentTarget);\n\n $(' ').attr('type', 'hidden')\n .attr('name', \"addonpage\")\n .attr('value', this.getAddOnPageId())\n .appendTo(formElement);\n });\n\n this.getModal().on('click', SELECTORS.ANCHOR, (e) => {\n const anchorElement = $(e.currentTarget);\n\n // If the anchor element was the add to quiz link.\n if (anchorElement.closest(SELECTORS.ADD_TO_QUIZ_CONTAINER).length) {\n this.handleAddToQuizEvent(e, anchorElement);\n return;\n }\n\n // If the anchor element was a preview question link.\n if (anchorElement.closest(SELECTORS.PREVIEW_CONTAINER).length) {\n this.handlePreviewContainerEvent(e, anchorElement);\n return;\n }\n\n // Sorting links have their own handler.\n if (anchorElement.closest(SELECTORS.SORTERS).length) {\n return;\n }\n\n // Anything else means reload the pop-up contents.\n e.preventDefault();\n this.reloadBodyContent(anchorElement.prop('search'));\n });\n\n // Disable the form change checker when the body is rendered.\n this.getRoot().on(ModalEvents.bodyRendered, () => {\n // Make sure the form change checker is disabled otherwise it'll stop the user from navigating away from the\n // page once the modal is hidden.\n FormChangeChecker.disableAllChecks();\n });\n }\n}\n\nModalQuizQuestionBank.registerModalType();\n"],"names":["SELECTORS","ModalQuizQuestionBank","Modal","constructor","root","contextId","addOnPageId","setContextId","id","getContextId","this","setAddOnPageId","getAddOnPageId","show","reloadBodyContent","window","location","search","super","querystring","setBody","Fragment","loadFragment","handleAddToQuizEvent","e","anchorElement","href","URL","attr","searchParams","set","handlePreviewContainerEvent","openpopup","url","name","options","join","registerEventListeners","getModal","on","formElement","currentTarget","appendTo","closest","length","preventDefault","prop","getRoot","ModalEvents","bodyRendered","FormChangeChecker","disableAllChecks","registerModalType"],"mappings":"2hDA6BMA,gCACqB,qBADrBA,iBAEM,UAFNA,4BAGiB,mBAHjBA,6BAIkB,sBAJlBA,kBAKO,iBAGQC,8BAA8BC,eAQ/CC,YAAYC,YACFA,WAEDC,UAAY,UACZC,YAAc,KAUvBC,aAAaC,SACJH,UAAYG,GASrBC,sBACWC,KAAKL,UAUhBM,eAAeH,SACNF,YAAcE,GASvBI,wBACWF,KAAKJ,YAahBO,mBACSC,kBAAkBC,OAAOC,SAASC,QAChCC,MAAML,KAAKH,MAatBI,kBAAkBK,kBAETC,QAAQC,SAASC,aAClB,WACA,qBACAZ,KAAKD,eACL,CACIU,YAAAA,eAaZI,qBAAqBC,EAAGC,qBAKdC,KAAO,IAAIC,IAAIF,cAAcG,KAAK,SACxCF,KAAKG,aAAaC,IAAI,YAAapB,KAAKE,kBACxCa,cAAcG,KAAK,OAAQF,MAU/BK,4BAA4BP,EAAGC,eAgB3BV,OAAOiB,UAAUR,EAAG,CAChBS,IAAKR,cAAcG,KAAK,QACxBM,KAAM,kBACNC,QAlBiB,CACjB,aACA,YACA,QACA,SACA,YACA,aACA,aACA,YACA,UACA,SACA,gBACA,eACA,aAKsBC,KAAK,OASnCC,+BAEUA,uBAAuB3B,WAExB4B,WAAWC,GAAG,SAAUvC,8BAA+BwB,UAIlDgB,aAAc,mBAAEhB,EAAEiB,mCAEtB,aAAab,KAAK,OAAQ,UACvBA,KAAK,OAAQ,aACbA,KAAK,QAASlB,KAAKE,kBACnB8B,SAASF,qBAGbF,WAAWC,GAAG,QAASvC,kBAAmBwB,UACrCC,eAAgB,mBAAED,EAAEiB,eAGtBhB,cAAckB,QAAQ3C,iCAAiC4C,YAClDrB,qBAAqBC,EAAGC,eAK7BA,cAAckB,QAAQ3C,6BAA6B4C,YAC9Cb,4BAA4BP,EAAGC,eAKpCA,cAAckB,QAAQ3C,mBAAmB4C,SAK7CpB,EAAEqB,sBACG/B,kBAAkBW,cAAcqB,KAAK,oBAIzCC,UAAUR,GAAGS,YAAYC,cAAc,KAGxCC,kBAAkBC,6FA/LZ,iDADGlD,6HAqMrBA,sBAAsBmD"}
\ No newline at end of file
diff --git a/mod/quiz/amd/build/update_random_question_filter_condition.min.js b/mod/quiz/amd/build/update_random_question_filter_condition.min.js
new file mode 100644
index 00000000000..42e55788bd2
--- /dev/null
+++ b/mod/quiz/amd/build/update_random_question_filter_condition.min.js
@@ -0,0 +1,12 @@
+define("mod_quiz/update_random_question_filter_condition",["exports","core/ajax","core/notification"],(function(_exports,_ajax,_notification){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
+/**
+ * Event handling for the edit random question form.
+ *
+ * Dynamically saves the new filter condition before navigating back to the quiz question list.
+ *
+ * @module mod_quiz/update_random_question_filter_condition
+ * @author 2022
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_ajax=_interopRequireDefault(_ajax),_notification=_interopRequireDefault(_notification);_exports.init=()=>{const SELECTORS_QUESTION_BANK_CONTAINER="#questionbank_container",SELECTORS_FORM_ELEMENT="#update_filter_condition_form",SELECTORS_UPDATE_BUTTON='[name="update"]',SELECTORS_MESSAGE_INPUT='[name="message"]',SELECTORS_FILTER_CONDITION_ELEMENT="[data-filtercondition]",questionBank=document.querySelector(SELECTORS_QUESTION_BANK_CONTAINER),form=document.querySelector(SELECTORS_FORM_ELEMENT);form.querySelector(SELECTORS_UPDATE_BUTTON).addEventListener("click",(async e=>{var _form$dataset,_form$dataset2,_questionBank$querySe;e.preventDefault();const request={methodname:"mod_quiz_update_filter_condition",args:{cmid:null===(_form$dataset=form.dataset)||void 0===_form$dataset?void 0:_form$dataset.cmid,slotid:null===(_form$dataset2=form.dataset)||void 0===_form$dataset2?void 0:_form$dataset2.slotid,filtercondition:null===(_questionBank$querySe=questionBank.querySelector(SELECTORS_FILTER_CONDITION_ELEMENT).dataset)||void 0===_questionBank$querySe?void 0:_questionBank$querySe.filtercondition}};try{const response=await _ajax.default.call([request])[0];form.querySelector(SELECTORS_MESSAGE_INPUT).value=response.message,form.submit()}catch(e){_notification.default.exception(e)}}))}}));
+
+//# sourceMappingURL=update_random_question_filter_condition.min.js.map
\ No newline at end of file
diff --git a/mod/quiz/amd/build/update_random_question_filter_condition.min.js.map b/mod/quiz/amd/build/update_random_question_filter_condition.min.js.map
new file mode 100644
index 00000000000..822c02476fd
--- /dev/null
+++ b/mod/quiz/amd/build/update_random_question_filter_condition.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"update_random_question_filter_condition.min.js","sources":["../src/update_random_question_filter_condition.js"],"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 .\n\n/**\n * Event handling for the edit random question form.\n *\n * Dynamically saves the new filter condition before navigating back to the quiz question list.\n *\n * @module mod_quiz/update_random_question_filter_condition\n * @author 2022 \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport ajax from 'core/ajax';\nimport Notification from 'core/notification';\n\nexport const init = () => {\n const SELECTORS = {\n QUESTION_BANK_CONTAINER: '#questionbank_container',\n FORM_ELEMENT: '#update_filter_condition_form',\n UPDATE_BUTTON: '[name=\"update\"]',\n CANCEL_BUTTON: '[name=\"cancel\"]',\n MESSAGE_INPUT: '[name=\"message\"]',\n FILTER_CONDITION_ELEMENT: '[data-filtercondition]',\n };\n\n const questionBank = document.querySelector(SELECTORS.QUESTION_BANK_CONTAINER);\n const form = document.querySelector(SELECTORS.FORM_ELEMENT);\n const updateButton = form.querySelector(SELECTORS.UPDATE_BUTTON);\n\n updateButton.addEventListener(\"click\", async(e) => {\n e.preventDefault();\n const request = {\n methodname: 'mod_quiz_update_filter_condition',\n args: {\n cmid: form.dataset?.cmid,\n slotid: form.dataset?.slotid,\n filtercondition: questionBank.querySelector(SELECTORS.FILTER_CONDITION_ELEMENT).dataset?.filtercondition,\n }\n };\n try {\n const response = await ajax.call([request])[0];\n const messageInput = form.querySelector(SELECTORS.MESSAGE_INPUT);\n messageInput.value = response.message;\n form.submit();\n } catch (e) {\n Notification.exception(e);\n }\n });\n\n};\n"],"names":["SELECTORS","questionBank","document","querySelector","form","addEventListener","async","e","preventDefault","request","methodname","args","cmid","dataset","_form$dataset","slotid","_form$dataset2","filtercondition","_questionBank$querySe","response","ajax","call","value","message","submit","exception"],"mappings":";;;;;;;;;wLA4BoB,WACVA,kCACuB,0BADvBA,uBAEY,gCAFZA,wBAGa,kBAHbA,wBAKa,mBALbA,mCAMwB,yBAGxBC,aAAeC,SAASC,cAAcH,mCACtCI,KAAOF,SAASC,cAAcH,wBACfI,KAAKD,cAAcH,yBAE3BK,iBAAiB,SAASC,MAAAA,2DACnCC,EAAEC,uBACIC,QAAU,CACZC,WAAY,mCACZC,KAAM,CACFC,2BAAMR,KAAKS,wCAALC,cAAcF,KACpBG,8BAAQX,KAAKS,yCAALG,eAAcD,OACtBE,8CAAiBhB,aAAaE,cAAcH,oCAAoCa,gDAA/DK,sBAAwED,4BAIvFE,eAAiBC,cAAKC,KAAK,CAACZ,UAAU,GACvBL,KAAKD,cAAcH,yBAC3BsB,MAAQH,SAASI,QAC9BnB,KAAKoB,SACP,MAAOjB,yBACQkB,UAAUlB"}
\ No newline at end of file
diff --git a/mod/quiz/amd/src/modal_add_random_question.js b/mod/quiz/amd/src/modal_add_random_question.js
index 7141de7b381..f1f7610aa41 100644
--- a/mod/quiz/amd/src/modal_add_random_question.js
+++ b/mod/quiz/amd/src/modal_add_random_question.js
@@ -27,18 +27,25 @@ import * as Notification from 'core/notification';
import * as Fragment from 'core/fragment';
import * as Templates from 'core/templates';
import * as FormChangeChecker from 'core_form/changechecker';
+import {call as fetchMany} from 'core/ajax';
const SELECTORS = {
EXISTING_CATEGORY_CONTAINER: '[data-region="existing-category-container"]',
- EXISTING_CATEGORY_FORM_ELEMENT: '#id_existingcategoryheader',
+ EXISTING_CATEGORY_TAB: '#id_existingcategoryheader',
NEW_CATEGORY_CONTAINER: '[data-region="new-category-container"]',
- NEW_CATEGORY_FORM_ELEMENT: '#id_newcategoryheader',
+ NEW_CATEGORY_TAB: '#id_newcategoryheader',
TAB_CONTENT: '[data-region="tab-content"]',
ADD_ON_PAGE_FORM_ELEMENT: '[name="addonpage"]',
- SUBMIT_BUTTON_ELEMENT: 'input[type="submit"]',
- CANCEL_BUTTON_ELEMENT: 'input[type="submit"][name="cancel"]',
+ ADD_RANDOM_BUTTON: 'input[type="submit"][name="addrandom"]',
+ ADD_NEW_CATEGORY_BUTTON: 'input[type="submit"][name="newcategory"]',
+ SUBMIT_BUTTON_ELEMENT: 'input[type="submit"][name="addrandom"], input[type="submit"][name="newcategory"]',
FORM_HEADER: 'legend',
- BUTTON_CONTAINER: '.fitem',
+ SELECT_NUMBER_TO_ADD: '#menurandomcount',
+ NEW_CATEGORY_ELEMENT: '#categoryname',
+ PARENT_CATEGORY_ELEMENT: '#parentcategory',
+ FILTER_CONDITION_ELEMENT: '[data-filtercondition]',
+ FORM_ELEMENT: '#add_random_question_form',
+ MESSAGE_INPUT: '[name="message"]',
};
export default class ModalAddRandomQuestion extends Modal {
@@ -173,24 +180,16 @@ export default class ModalAddRandomQuestion extends Modal {
* Moves the submit button into a footer element at the bottom of the form
* element for styling purposes.
*
- * @method moveFormElementIntoTab
- * @param {jquery} formElement The form element to move into the tab.
+ * @method moveContentIntoTab
+ * @param {jquery} tabContent The form element to move into the tab.
* @param {jquey} tabElement The tab element for the form element to move into.
*/
- moveFormElementIntoTab(formElement, tabElement) {
- const submitButtons = formElement.find(SELECTORS.SUBMIT_BUTTON_ELEMENT);
- const footer = $('');
+ moveContentIntoTab(tabContent, tabElement) {
// Hide the header because the tabs show us which part of the form we're
// looking at.
- formElement.find(SELECTORS.FORM_HEADER).addClass('hidden');
+ tabContent.find(SELECTORS.FORM_HEADER).addClass('hidden');
// Move the element inside a tab.
- formElement.wrap(tabElement);
- // Remove the buttons container element.
- submitButtons.closest(SELECTORS.BUTTON_CONTAINER).remove();
- // Put the button inside a footer.
- submitButtons.appendTo(footer);
- // Add the footer to the end of the category form element.
- footer.appendTo(formElement);
+ tabContent.wrap(tabElement);
}
/**
@@ -230,29 +229,33 @@ export default class ModalAddRandomQuestion extends Modal {
* @return {promise} Resolved with form HTML and JS.
*/
loadForm() {
+ const cmid = this.getCMID();
+ const cat = this.getCategory();
+ const addonpage = this.getAddOnPageId();
+ const returnurl = this.getReturnUrl();
+
return Fragment.loadFragment(
'mod_quiz',
'add_random_question_form',
this.getContextId(),
{
- addonpage: this.getAddOnPageId(),
- cat: this.getCategory(),
- returnurl: this.getReturnUrl(),
- cmid: this.getCMID()
+ addonpage,
+ cat,
+ returnurl,
+ cmid,
}
)
.then((html, js) =>{
const form = $(html);
- const existingCategoryFormElement = form.find(SELECTORS.EXISTING_CATEGORY_FORM_ELEMENT);
+ const existingCategoryTabContent = form.find(SELECTORS.EXISTING_CATEGORY_TAB);
const existingCategoryTab = this.getBody().find(SELECTORS.EXISTING_CATEGORY_CONTAINER);
- const newCategoryFormElement = form.find(SELECTORS.NEW_CATEGORY_FORM_ELEMENT);
+ const newCategoryTabContent = form.find(SELECTORS.NEW_CATEGORY_TAB);
const newCategoryTab = this.getBody().find(SELECTORS.NEW_CATEGORY_CONTAINER);
// Transform the form into tabs for better rendering in the modal.
- this.moveFormElementIntoTab(existingCategoryFormElement, existingCategoryTab);
- this.moveFormElementIntoTab(newCategoryFormElement, newCategoryTab);
+ this.moveContentIntoTab(existingCategoryTabContent, existingCategoryTab);
+ this.moveContentIntoTab(newCategoryTabContent, newCategoryTab);
this.moveTabsIntoTabContent(form);
- this.moveCancelButtonToTabs(form);
Templates.replaceNode(this.getBody().find(SELECTORS.TAB_CONTENT), form, js);
return;
@@ -261,11 +264,82 @@ export default class ModalAddRandomQuestion extends Modal {
// Make sure the form change checker is disabled otherwise it'll stop the user from navigating away from the
// page once the modal is hidden.
FormChangeChecker.disableAllChecks();
- return;
+
+ // Add question to quiz.
+ this.getBody()[0].addEventListener('click', (e) => {
+ const button = e.target.closest(SELECTORS.SUBMIT_BUTTON_ELEMENT);
+ if (!button) {
+ return;
+ }
+ e.preventDefault();
+
+ const randomcount = document.querySelector(SELECTORS.SELECT_NUMBER_TO_ADD).value;
+ const filtercondition = document.querySelector(SELECTORS.FILTER_CONDITION_ELEMENT).dataset?.filtercondition;
+
+ // Add Random questions if the add random button was clicked.
+ const addRandomButton = e.target.closest(SELECTORS.ADD_RANDOM_BUTTON);
+ if (addRandomButton) {
+ this.addQuestions(cmid, addonpage, randomcount, filtercondition, '', '');
+ return;
+ }
+ // Add new category if the add category button was clicked.
+ const addCategoryButton = e.target.closest(SELECTORS.ADD_NEW_CATEGORY_BUTTON);
+ if (addCategoryButton) {
+ this.addQuestions(
+ cmid,
+ addonpage,
+ randomcount,
+ filtercondition,
+ document.querySelector(SELECTORS.NEW_CATEGORY_ELEMENT).value,
+ document.querySelector(SELECTORS.PARENT_CATEGORY_ELEMENT).value
+ );
+ return;
+ }
+ });
})
.catch(Notification.exception);
}
+ /**
+ * Call web service function to add random questions
+ *
+ * @param {number} cmid course module id
+ * @param {number} addonpage the page where random questions will be added to
+ * @param {number} randomcount Number of random questions
+ * @param {string} filtercondition Filter condition
+ * @param {string} newcategory add new category
+ * @param {string} parentcategory parent category of new category
+ */
+ async addQuestions(
+ cmid,
+ addonpage,
+ randomcount,
+ filtercondition,
+ newcategory,
+ parentcategory
+ ) {
+ const call = {
+ methodname: 'mod_quiz_add_random_questions',
+ args: {
+ cmid,
+ addonpage,
+ randomcount,
+ filtercondition,
+ newcategory,
+ parentcategory,
+ }
+ };
+ try {
+ const response = await fetchMany([call])[0];
+ const form = document.querySelector(SELECTORS.FORM_ELEMENT);
+ const messageInput = form.querySelector(SELECTORS.MESSAGE_INPUT);
+ messageInput.value = response.message;
+ form.submit();
+ } catch (e) {
+ Notification.exception(e);
+ }
+ }
+
/**
* Override the modal show function to load the form when this modal is first
* shown.
@@ -276,7 +350,7 @@ export default class ModalAddRandomQuestion extends Modal {
super.show(this);
if (!this.loadedForm) {
- this.loadForm();
+ this.loadForm(window.location.search);
this.loadedForm = true;
}
}
diff --git a/mod/quiz/amd/src/modal_quiz_question_bank.js b/mod/quiz/amd/src/modal_quiz_question_bank.js
index 3add9296122..5ebd4661d42 100644
--- a/mod/quiz/amd/src/modal_quiz_question_bank.js
+++ b/mod/quiz/amd/src/modal_quiz_question_bank.js
@@ -31,9 +31,8 @@ const SELECTORS = {
ADD_TO_QUIZ_CONTAINER: 'td.addtoquizaction',
ANCHOR: 'a[href]',
PREVIEW_CONTAINER: 'td.previewaction',
- SEARCH_OPTIONS: '#advancedsearch',
- DISPLAY_OPTIONS: '#displayoptions',
ADD_QUESTIONS_FORM: 'form#questionsubmit',
+ SORTERS: '.sorters',
};
export default class ModalQuizQuestionBank extends Modal {
@@ -178,59 +177,6 @@ export default class ModalQuizQuestionBank extends Modal {
});
}
- /**
- * Reload the modal body with the new display options the user has selected.
- *
- * A query string is built using the form elements to be used to generate the
- * new body content.
- *
- * @method handleDisplayOptionFormEvent
- * @param {event} e A JavaScript event
- */
- handleDisplayOptionFormEvent(e) {
- // Stop propagation to prevent other wild event handlers
- // from submitting the form on change.
- e.stopPropagation();
- e.preventDefault();
-
- const form = $(e.target).closest(SELECTORS.DISPLAY_OPTIONS);
- const queryString = '?' + form.serialize();
- this.reloadBodyContent(queryString);
- }
-
- /**
- * Listen for changes to the display options form.
- *
- * This handles the user changing:
- * - The quiz category select box
- * - The tags to filter by
- * - Show/hide questions from sub categories
- * - Show/hide old questions
- *
- * @method registerDisplayOptionListeners
- */
- registerDisplayOptionListeners() {
- // Listen for changes to the display options form.
- this.getModal().on('change', SELECTORS.DISPLAY_OPTIONS, (e) => {
- // Get the element that was changed.
- const modifiedElement = $(e.target);
- if (modifiedElement.attr('aria-autocomplete')) {
- // If the element that was change is the autocomplete
- // input then we should ignore it because that is for
- // display purposes only.
- return;
- }
-
- this.handleDisplayOptionFormEvent(e);
- });
-
- // Listen for the display options form submission because the tags
- // filter will submit the form when it is changed.
- this.getModal().on('submit', SELECTORS.DISPLAY_OPTIONS, (e) => {
- this.handleDisplayOptionFormEvent(e);
- });
- }
-
/**
* Set up all of the event handling for the modal.
*
@@ -240,9 +186,6 @@ export default class ModalQuizQuestionBank extends Modal {
// Apply parent event listeners.
super.registerEventListeners(this);
- // Set up the event handlers for all of the display options.
- this.registerDisplayOptionListeners();
-
this.getModal().on('submit', SELECTORS.ADD_QUESTIONS_FORM, (e) => {
// If the user clicks on the "Add selected questions to the quiz" button to add some questions to the page
// then we need to intercept the submit in order to include the correct "add on page id" before the form is
@@ -270,9 +213,8 @@ export default class ModalQuizQuestionBank extends Modal {
return;
}
- // Click on expand/collaspse search-options. Has its own handler.
- // We should not interfere.
- if (anchorElement.closest(SELECTORS.SEARCH_OPTIONS).length) {
+ // Sorting links have their own handler.
+ if (anchorElement.closest(SELECTORS.SORTERS).length) {
return;
}
diff --git a/mod/quiz/amd/src/update_random_question_filter_condition.js b/mod/quiz/amd/src/update_random_question_filter_condition.js
new file mode 100644
index 00000000000..eeea510f22c
--- /dev/null
+++ b/mod/quiz/amd/src/update_random_question_filter_condition.js
@@ -0,0 +1,63 @@
+// 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 .
+
+/**
+ * Event handling for the edit random question form.
+ *
+ * Dynamically saves the new filter condition before navigating back to the quiz question list.
+ *
+ * @module mod_quiz/update_random_question_filter_condition
+ * @author 2022
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import ajax from 'core/ajax';
+import Notification from 'core/notification';
+
+export const init = () => {
+ const SELECTORS = {
+ QUESTION_BANK_CONTAINER: '#questionbank_container',
+ FORM_ELEMENT: '#update_filter_condition_form',
+ UPDATE_BUTTON: '[name="update"]',
+ CANCEL_BUTTON: '[name="cancel"]',
+ MESSAGE_INPUT: '[name="message"]',
+ FILTER_CONDITION_ELEMENT: '[data-filtercondition]',
+ };
+
+ const questionBank = document.querySelector(SELECTORS.QUESTION_BANK_CONTAINER);
+ const form = document.querySelector(SELECTORS.FORM_ELEMENT);
+ const updateButton = form.querySelector(SELECTORS.UPDATE_BUTTON);
+
+ updateButton.addEventListener("click", async(e) => {
+ e.preventDefault();
+ const request = {
+ methodname: 'mod_quiz_update_filter_condition',
+ args: {
+ cmid: form.dataset?.cmid,
+ slotid: form.dataset?.slotid,
+ filtercondition: questionBank.querySelector(SELECTORS.FILTER_CONDITION_ELEMENT).dataset?.filtercondition,
+ }
+ };
+ try {
+ const response = await ajax.call([request])[0];
+ const messageInput = form.querySelector(SELECTORS.MESSAGE_INPUT);
+ messageInput.value = response.message;
+ form.submit();
+ } catch (e) {
+ Notification.exception(e);
+ }
+ });
+
+};
diff --git a/mod/quiz/classes/external/add_random_questions.php b/mod/quiz/classes/external/add_random_questions.php
new file mode 100644
index 00000000000..88a62e30520
--- /dev/null
+++ b/mod/quiz/classes/external/add_random_questions.php
@@ -0,0 +1,158 @@
+.
+
+namespace mod_quiz\external;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir . '/externallib.php');
+require_once($CFG->dirroot . '/question/editlib.php');
+require_once($CFG->dirroot . '/mod/quiz/locallib.php');
+
+use external_function_parameters;
+use external_single_structure;
+use external_value;
+use external_api;
+use mod_quiz\question\bank\filter\custom_category_condition;
+use mod_quiz\quiz_settings;
+use mod_quiz\structure;
+
+/**
+ * Add random questions to a quiz.
+ *
+ * @package mod_quiz
+ * @copyright 2022 Catalyst IT Australia Pty Ltd
+ * @author Nathan Nguyen
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class add_random_questions extends external_api {
+
+ /**
+ * Parameters for the web service function
+ *
+ * @return external_function_parameters
+ */
+ public static function execute_parameters(): external_function_parameters {
+ return new external_function_parameters (
+ [
+ 'cmid' => new external_value(PARAM_INT, 'The cmid of the quiz'),
+ 'addonpage' => new external_value(PARAM_INT, 'The page where random questions will be added to'),
+ 'randomcount' => new external_value(PARAM_INT, 'Number of random questions'),
+ 'filtercondition' => new external_value(PARAM_TEXT, 'Filter condition'),
+ 'newcategory' => new external_value(
+ PARAM_TEXT,
+ '(Optional) The name of a new question category to create and use for the random questions.',
+ VALUE_DEFAULT,
+ '',
+ ),
+ 'parentcategory' => new external_value(
+ PARAM_TEXT,
+ '(Optional) The parent of the new question category, if creating one.',
+ VALUE_DEFAULT,
+ 0,
+ ),
+ ]
+ );
+ }
+
+ /**
+ * Add random questions.
+ *
+ * @param int $cmid The cmid of the quiz
+ * @param int $addonpage The page where random questions will be added to
+ * @param int $randomcount Number of random questions
+ * @param string $filtercondition Filter condition
+ * @param string $newcategory add new category
+ * @param string $parentcategory parent category of new category
+ * @return array result
+ */
+ public static function execute(
+ int $cmid,
+ int $addonpage,
+ int $randomcount,
+ string $filtercondition,
+ string $newcategory = '',
+ string $parentcategory = '',
+ ): array {
+ [
+ 'cmid' => $cmid,
+ 'addonpage' => $addonpage,
+ 'randomcount' => $randomcount,
+ 'filtercondition' => $filtercondition,
+ 'newcategory' => $newcategory,
+ 'parentcategory' => $parentcategory,
+ ] = self::validate_parameters(self::execute_parameters(), [
+ 'cmid' => $cmid,
+ 'addonpage' => $addonpage,
+ 'randomcount' => $randomcount,
+ 'filtercondition' => $filtercondition,
+ 'newcategory' => $newcategory,
+ 'parentcategory' => $parentcategory,
+ ]);
+
+ // Validate context.
+ $thiscontext = \context_module::instance($cmid);
+ self::validate_context($thiscontext);
+ require_capability('mod/quiz:manage', $thiscontext);
+
+ $filtercondition = json_decode($filtercondition, true);
+
+ // Create new category.
+ if (!empty($newcategory)) {
+ $contexts = new \core_question\local\bank\question_edit_contexts($thiscontext);
+ $defaultcategoryobj = question_make_default_categories($contexts->all());
+ $defaultcategory = $defaultcategoryobj->id . ',' . $defaultcategoryobj->contextid;
+ $qcobject = new \qbank_managecategories\question_category_object(
+ null,
+ null,
+ $contexts->having_one_edit_tab_cap('categories'),
+ $defaultcategoryobj->id,
+ $defaultcategory,
+ null,
+ $contexts->having_cap('moodle/question:add'));
+ $categoryid = $qcobject->add_category($parentcategory, $newcategory, '', true);
+ $filter = [
+ 'category' => [
+ 'jointype' => custom_category_condition::JOINTYPE_DEFAULT,
+ 'values' => [$categoryid],
+ 'filteroptions' => ['includesubcategories' => false],
+ ]
+ ];
+ $filtercondition['filter'] = $filter;
+ }
+
+ // Add random question to the quiz.
+ [$quiz, ] = get_module_from_cmid($cmid);
+ $settings = quiz_settings::create_for_cmid($cmid);
+ $structure = structure::create_for_quiz($settings);
+ $structure->add_random_questions($addonpage, $randomcount, $filtercondition);
+ quiz_delete_previews($quiz);
+ quiz_settings::create($quiz->id)->get_grade_calculator()->recompute_quiz_sumgrades();
+
+ return ['message' => get_string('addarandomquestion_success', 'mod_quiz')];
+ }
+
+ /**
+ * Returns description of method result value.
+ *
+ * @return external_value
+ */
+ public static function execute_returns() {
+ return new external_single_structure([
+ 'message' => new external_value(PARAM_TEXT, 'Message', VALUE_OPTIONAL)
+ ]);
+ }
+}
diff --git a/mod/quiz/classes/external/update_filter_condition.php b/mod/quiz/classes/external/update_filter_condition.php
new file mode 100644
index 00000000000..70d2c5c94bf
--- /dev/null
+++ b/mod/quiz/classes/external/update_filter_condition.php
@@ -0,0 +1,104 @@
+.
+
+namespace mod_quiz\external;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir . '/externallib.php');
+require_once($CFG->dirroot . '/question/editlib.php');
+require_once($CFG->dirroot . '/mod/quiz/locallib.php');
+
+use external_function_parameters;
+use external_single_structure;
+use external_value;
+use external_api;
+
+/**
+ * Update the filter condition for a random question.
+ *
+ * @package mod_quiz
+ * @copyright 2022 Catalyst IT Australia Pty Ltd
+ * @author Nathan Nguyen
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class update_filter_condition extends external_api {
+
+ /**
+ * Parameters for the web service function
+ *
+ * @return external_function_parameters
+ */
+ public static function execute_parameters(): external_function_parameters {
+ return new external_function_parameters ([
+ 'cmid' => new external_value(PARAM_INT, 'The cmid of the quiz'),
+ 'slotid' => new external_value(PARAM_INT, 'The quiz slot ID for the random question.'),
+ 'filtercondition' => new external_value(PARAM_TEXT, 'Filter condition'),
+ ]);
+ }
+
+ /**
+ * Add random questions.
+ *
+ * @param int $cmid course module id
+ * @param int $slotid The quiz slot id
+ * @param string $filtercondition
+ * @return array result
+ */
+ public static function execute(
+ int $cmid,
+ int $slotid,
+ string $filtercondition,
+ ): array {
+ global $DB;
+
+ [
+ 'cmid' => $cmid,
+ 'slotid' => $slotid,
+ 'filtercondition' => $filtercondition,
+ ] = self::validate_parameters(self::execute_parameters(), [
+ 'cmid' => $cmid,
+ 'slotid' => $slotid,
+ 'filtercondition' => $filtercondition,
+ ]);
+
+ // Validate context.
+ $thiscontext = \context_module::instance($cmid);
+ self::validate_context($thiscontext);
+ require_capability('mod/quiz:manage', $thiscontext);
+
+ // Update filter condition.
+ $setparams = [
+ 'itemid' => $slotid,
+ 'questionarea' => 'slot',
+ 'component' => 'mod_quiz',
+ ];
+ $DB->set_field('question_set_references', 'filtercondition', $filtercondition, $setparams);
+
+ return ['message' => get_string('updatefilterconditon_success', 'mod_quiz')];
+ }
+
+ /**
+ * Returns description of method result value.
+ *
+ * @return external_value
+ */
+ public static function execute_returns() {
+ return new external_single_structure([
+ 'message' => new external_value(PARAM_TEXT, 'Message', VALUE_OPTIONAL)
+ ]);
+ }
+}
diff --git a/mod/quiz/classes/form/add_random_form.php b/mod/quiz/classes/form/add_random_form.php
index d90d2495602..e9336ea117e 100644
--- a/mod/quiz/classes/form/add_random_form.php
+++ b/mod/quiz/classes/form/add_random_form.php
@@ -16,6 +16,7 @@
namespace mod_quiz\form;
+use core\check\performance\debugging;
use core_tag_tag;
use moodleform;
@@ -30,10 +31,23 @@ require_once($CFG->libdir.'/formslib.php');
* @package mod_quiz
* @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @deprecated Moodle 4.3 MDL-72321. This form is new generated in a modal with mod_quiz/add_random_question_form.mustache
+ * @todo Final deprecation in Moodle 4.7 MDL-78091
*/
class add_random_form extends moodleform {
+ /**
+ * Deprecated.
+ *
+ * @return void
+ * @deprecated Moodle 4.3 MDL-72321
+ * @todo Final deprecation in Moodle 4.7 MDL-78091
+ */
protected function definition() {
+ debugging(
+ 'add_random_form is deprecated. Please use mod_quiz/add_random_question_form.mustache instead.',
+ DEBUG_DEVELOPER
+ );
global $OUTPUT, $PAGE, $CFG;
$mform = $this->_form;
@@ -119,7 +133,20 @@ class add_random_form extends moodleform {
]);
}
+ /**
+ * Deprecated.
+ *
+ * @param array $fromform
+ * @param array $files
+ * @return array
+ * @deprecated Moodle 4.3 MDL-72321
+ * @todo Final deprecation in Moodle 4.7 MDL-78091
+ */
public function validation($fromform, $files) {
+ debugging(
+ 'add_random_form is deprecated. Please use mod_quiz/add_random_question_form.mustache instead.',
+ DEBUG_DEVELOPER
+ );
$errors = parent::validation($fromform, $files);
if (!empty($fromform['newcategory']) && trim($fromform['name']) == '') {
diff --git a/mod/quiz/classes/local/structure/slot_random.php b/mod/quiz/classes/local/structure/slot_random.php
index f2e116e63a6..a743f6eaaff 100644
--- a/mod/quiz/classes/local/structure/slot_random.php
+++ b/mod/quiz/classes/local/structure/slot_random.php
@@ -112,8 +112,13 @@ class slot_random {
* Set some tags for this quiz slot.
*
* @param \core_tag_tag[] $tags
+ *
+ * @deprecated since Moodle 4.3
+ * @todo Final deprecation on Moodle 4.7 MDL-78091
*/
public function set_tags($tags) {
+ debugging('Method set_tags() is deprecated, ' .
+ 'please do not use this function.', DEBUG_DEVELOPER);
$this->tags = [];
foreach ($tags as $tag) {
// We use $tag->id as the key for the array so not only it handles duplicates of the same tag being given,
@@ -126,22 +131,24 @@ class slot_random {
* Set some tags for this quiz slot. This function uses tag ids to find tags.
*
* @param int[] $tagids
+ * @deprecated since Moodle 4.3
+ * @todo Final deprecation on Moodle 4.7 MDL-78091
*/
public function set_tags_by_id($tagids) {
+ debugging(
+ 'Method set_tags_by_id() is deprecated, please do not use this function.',
+ DEBUG_DEVELOPER
+ );
$this->tags = \core_tag_tag::get_bulk($tagids, 'id, name');
}
/**
* Set filter condition.
*
- * @param \stdClass $filters
+ * @param \string $filters
*/
- public function set_filter_condition($filters) {
- if (!empty($this->tags)) {
- $filters->tags = $this->tags;
- }
-
- $this->filtercondition = json_encode($filters);
+ public function set_filter_condition(string $filters): void {
+ $this->filtercondition = $filters;
}
/**
diff --git a/mod/quiz/classes/output/edit_renderer.php b/mod/quiz/classes/output/edit_renderer.php
index cc96d41c9da..498813f73b6 100644
--- a/mod/quiz/classes/output/edit_renderer.php
+++ b/mod/quiz/classes/output/edit_renderer.php
@@ -680,7 +680,7 @@ class edit_renderer extends \plugin_renderer_base {
if ($structure->can_add_random_questions()) {
$returnurl = new \moodle_url('/mod/quiz/edit.php', ['cmid' => $structure->get_cmid(), 'data-addonpage' => $page]);
$params = ['returnurl' => $returnurl, 'cmid' => $structure->get_cmid(), 'appendqnumstring' => 'addarandomquestion'];
- $url = new \moodle_url('/mod/quiz/addrandom.php', $params);
+ $url = new \moodle_url('/mod/quiz/edit.php', $params);
$icon = new \pix_icon('t/add', $str->addarandomquestion, 'moodle', ['class' => 'iconsmall', 'title' => '']);
$attributes = ['class' => 'cm-edit-action addarandomquestion', 'data-action' => 'addarandomquestion'];
if ($page) {
@@ -1079,7 +1079,6 @@ class edit_renderer extends \plugin_renderer_base {
$qbankurlparams = [
'cmid' => $structure->get_cmid(),
'cat' => $slot->category . ',' . $slot->contextid,
- 'recurse' => $slot->randomrecurse,
];
$slottags = [];
diff --git a/mod/quiz/classes/question/bank/custom_view.php b/mod/quiz/classes/question/bank/custom_view.php
index 77f587a6b2f..694f3bdab7b 100644
--- a/mod/quiz/classes/question/bank/custom_view.php
+++ b/mod/quiz/classes/question/bank/custom_view.php
@@ -25,9 +25,15 @@
namespace mod_quiz\question\bank;
+defined('MOODLE_INTERNAL') || die();
+
+use core\output\datafilter;
+use core_question\local\bank\condition;
use core_question\local\bank\question_version_status;
use mod_quiz\question\bank\filter\custom_category_condition;
+use qbank_managecategories\category_condition;
+require_once($CFG->dirroot . '/mod/quiz/locallib.php');
/**
* Subclass to customise the view of the question bank for the quiz editing screen.
*
@@ -45,8 +51,10 @@ class custom_view extends \core_question\local\bank\view {
/** @var \stdClass $quiz the quiz settings. */
protected $quiz = false;
- /** @var int The maximum displayed length of the category info. */
- const MAX_TEXT_LENGTH = 200;
+ /**
+ * @var string $component the component the api is used from.
+ */
+ public $component = 'mod_quiz';
/**
* Constructor.
@@ -56,27 +64,55 @@ class custom_view extends \core_question\local\bank\view {
* @param \stdClass $cm activity settings.
* @param \stdClass $quiz quiz settings.
*/
- public function __construct($contexts, $pageurl, $course, $cm, $quiz) {
- parent::__construct($contexts, $pageurl, $course, $cm);
- $this->quiz = $quiz;
+ public function __construct($contexts, $pageurl, $course, $cm, $params, $extraparams) {
+ // Default filter condition.
+ if (!isset($params['filter'])) {
+ $params['filter'] = [];
+ [$categoryid, $contextid] = custom_category_condition::validate_category_param($params['cat']);
+ if (!is_null($categoryid)) {
+ $category = custom_category_condition::get_category_record($categoryid, $contextid);
+ $params['filter']['category'] = [
+ 'jointype' => custom_category_condition::JOINTYPE_DEFAULT,
+ 'values' => [$category->id],
+ 'filteroptions' => ['includesubcategories' => false],
+ ];
+ }
+ }
+
+ $this->init_required_columns();
+ parent::__construct($contexts, $pageurl, $course, $cm, $params, $extraparams);
+ [$this->quiz, ] = get_module_from_cmid($cm->id);
+ $this->set_quiz_has_attempts(quiz_has_attempts($this->quiz->id));
$this->pagesize = self::DEFAULT_PAGE_SIZE;
}
- protected function get_question_bank_plugins(): array {
- $questionbankclasscolumns = [];
- $corequestionbankcolumns = [
+ /**
+ * Init required columns.
+ *
+ * @return void
+ */
+ protected function init_required_columns(): void {
+ // Override core columns.
+ $this->corequestionbankcolumns = [
'add_action_column',
'checkbox_column',
'question_type_column',
'question_name_text_column',
'preview_action_column'
];
+ }
+ /**
+ * Get class for each question bank columns.
+ *
+ * @return array
+ */
+ protected function get_class_for_columns(): array {
+ $questionbankclasscolumns = [];
if (question_get_display_preference('qbshowtext', 0, PARAM_INT, new \moodle_url(''))) {
- $corequestionbankcolumns[] = 'question_text_row';
+ $this->corequestionbankcolumns[] = 'question_text_row';
}
-
- foreach ($corequestionbankcolumns as $fullname) {
+ foreach ($this->corequestionbankcolumns as $fullname) {
$shortname = $fullname;
if (class_exists('mod_quiz\\question\\bank\\' . $fullname)) {
$fullname = 'mod_quiz\\question\\bank\\' . $fullname;
@@ -88,40 +124,18 @@ class custom_view extends \core_question\local\bank\view {
$questionbankclasscolumns[$shortname] = '';
}
}
- $plugins = \core_component::get_plugin_list_with_class('qbank', 'plugin_feature', 'plugin_feature.php');
- foreach ($plugins as $componentname => $plugin) {
- $pluginentrypointobject = new $plugin();
- $plugincolumnobjects = $pluginentrypointobject->get_question_columns($this);
- // Don't need the plugins without column objects.
- if (empty($plugincolumnobjects)) {
- unset($plugins[$componentname]);
- continue;
- }
- foreach ($plugincolumnobjects as $columnobject) {
- $columnname = $columnobject->get_column_name();
- foreach ($corequestionbankcolumns as $key => $corequestionbankcolumn) {
- if (!\core\plugininfo\qbank::is_plugin_enabled($componentname)) {
- unset($questionbankclasscolumns[$columnname]);
- continue;
- }
- // Check if it has custom preference selector to view/hide.
- if ($columnobject->has_preference() && !$columnobject->get_preference()) {
- continue;
- }
- if ($corequestionbankcolumn === $columnname) {
- $questionbankclasscolumns[$columnname] = $columnobject;
- }
- }
- }
- }
-
- // Mitigate the error in case of any regression.
- foreach ($questionbankclasscolumns as $shortname => $questionbankclasscolumn) {
- if (empty($questionbankclasscolumn)) {
- unset($questionbankclasscolumns[$shortname]);
- }
- }
+ return $questionbankclasscolumns;
+ }
+ /**
+ * Get the list of qbank plugins with available objects for features.
+ *
+ * @return array
+ */
+ protected function get_question_bank_plugins(): array {
+ // The view is too crowded, exclude some columns.
+ $questionbankclasscolumns = parent::get_question_bank_plugins();
+ $questionbankclasscolumns = array_intersect_key($questionbankclasscolumns, array_flip($this->corequestionbankcolumns));
return $questionbankclasscolumns;
}
@@ -132,8 +146,8 @@ class custom_view extends \core_question\local\bank\view {
protected function default_sort(): array {
// Using the extended class for quiz specific sort.
return [
- 'qbank_viewquestiontype\\question_type_column' => 1,
- 'mod_quiz\\question\\bank\\question_name_text_column' => 1,
+ 'qbank_viewquestiontype__question_type_column' => SORT_ASC,
+ 'mod_quiz__question__bank__question_name_text_column' => SORT_ASC,
];
}
@@ -144,23 +158,13 @@ class custom_view extends \core_question\local\bank\view {
*
* @param bool $quizhasattempts whether the quiz has attempts.
*/
- public function set_quiz_has_attempts($quizhasattempts): void {
+ private function set_quiz_has_attempts($quizhasattempts): void {
$this->quizhasattempts = $quizhasattempts;
if ($quizhasattempts && isset($this->visiblecolumns['addtoquizaction'])) {
unset($this->visiblecolumns['addtoquizaction']);
}
}
- /**
- * Question preview url.
- *
- * @param \stdClass $question
- * @return \moodle_url
- */
- public function preview_question_url($question) {
- return quiz_question_preview_url($this->quiz, $question);
- }
-
/**
* URL of add to quiz.
*
@@ -171,6 +175,7 @@ class custom_view extends \core_question\local\bank\view {
$params = $this->baseurl->params();
$params['addquestion'] = $questionid;
$params['sesskey'] = sesskey();
+ $params['cmid'] = $this->cm->id;
return new \moodle_url('/mod/quiz/edit.php', $params);
}
@@ -186,7 +191,7 @@ class custom_view extends \core_question\local\bank\view {
*/
public function render($pagevars, $tabname): string {
ob_start();
- $this->display($pagevars, $tabname);
+ $this->display();
$out = ob_get_contents();
ob_end_clean();
return $out;
@@ -216,8 +221,15 @@ class custom_view extends \core_question\local\bank\view {
echo \html_writer::end_tag('div');
}
+ /**
+ * Override the base implementation in \core_question\local\bank\view
+ * because we don't want to print new question form in the fragment
+ * for the modal.
+ *
+ * @param false|mixed|\stdClass $category
+ * @param bool $canadd
+ */
protected function create_new_question_form($category, $canadd): void {
- // Don't display this.
}
/**
@@ -228,45 +240,15 @@ class custom_view extends \core_question\local\bank\view {
protected function display_question_bank_header(): void {
}
- /**
- * Override the base implementation in \core_question\bank\view
- * because we don't want it to read from the $_POST global variables
- * for the sort parameters since they are not present in a fragment.
- *
- * Unfortunately the best we can do is to look at the URL for
- * those parameters (only marginally better really).
- */
- protected function init_sort_from_params(): void {
- $this->sort = [];
- for ($i = 1; $i <= self::MAX_SORTS; $i++) {
- if (!$sort = $this->baseurl->param('qbs' . $i)) {
- break;
- }
- // Work out the appropriate order.
- $order = 1;
- if ($sort[0] == '-') {
- $order = -1;
- $sort = substr($sort, 1);
- if (!$sort) {
- break;
- }
- }
- // Deal with subsorts.
- list($colname) = $this->parse_subsort($sort);
- $this->get_column_type($colname);
- $this->sort[$sort] = $order;
- }
- }
-
protected function build_query(): void {
// Get the required tables and fields.
[$fields, $joins] = $this->get_component_requirements(array_merge($this->requiredcolumns, $this->questionactions));
// Build the order by clause.
$sorts = [];
- foreach ($this->sort as $sort => $order) {
- list($colname, $subsort) = $this->parse_subsort($sort);
- $sorts[] = $this->requiredcolumns[$colname]->sort_expression($order < 0, $subsort);
+ foreach ($this->sort as $sortname => $sortorder) {
+ [$colname, $subsort] = $this->parse_subsort($sortname);
+ $sorts[] = $this->requiredcolumns[$colname]->sort_expression($sortorder == SORT_DESC, $subsort);
}
// Build the where clause.
@@ -275,50 +257,50 @@ class custom_view extends \core_question\local\bank\view {
JOIN {question_bank_entries} be
ON be.id = v.questionbankentryid
WHERE be.id = qbe.id)';
- $readyonly = "qv.status = '" . question_version_status::QUESTION_STATUS_READY . "' ";
- $tests = ['q.parent = 0', $latestversion, $readyonly];
+ $onlyready = '((' . "qv.status = '" . question_version_status::QUESTION_STATUS_READY . "'" .'))';
$this->sqlparams = [];
+ $conditions = [];
foreach ($this->searchconditions as $searchcondition) {
if ($searchcondition->where()) {
- $tests[] = '((' . $searchcondition->where() .'))';
+ $conditions[] = '((' . $searchcondition->where() .'))';
}
if ($searchcondition->params()) {
$this->sqlparams = array_merge($this->sqlparams, $searchcondition->params());
}
}
+ $majorconditions = ['q.parent = 0', $latestversion, $onlyready];
+ // Get higher level filter condition.
+ $jointype = isset($this->pagevars['jointype']) ? (int)$this->pagevars['jointype'] : condition::JOINTYPE_DEFAULT;
+ $nonecondition = ($jointype === datafilter::JOINTYPE_NONE) ? ' NOT ' : '';
+ $separator = ($jointype === datafilter::JOINTYPE_ALL) ? ' AND ' : ' OR ';
// Build the SQL.
$sql = ' FROM {question} q ' . implode(' ', $joins);
- $sql .= ' WHERE ' . implode(' AND ', $tests);
+ $sql .= ' WHERE ' . implode(' AND ', $majorconditions);
+ if (!empty($conditions)) {
+ $sql .= ' AND ' . $nonecondition . ' ( ';
+ $sql .= implode($separator, $conditions);
+ $sql .= ' ) ';
+ }
$this->countsql = 'SELECT count(1)' . $sql;
$this->loadsql = 'SELECT ' . implode(', ', $fields) . $sql . ' ORDER BY ' . implode(', ', $sorts);
}
- public function wanted_filters($cat, $tagids, $showhidden, $recurse, $editcontexts, $showquestiontext): void {
- global $CFG;
- list(, $contextid) = explode(',', $cat);
- $catcontext = \context::instance_by_id($contextid);
- $thiscontext = $this->get_most_specific_context();
- // Category selection form.
- $this->display_question_bank_header();
-
- // Display tag filter if usetags setting is enabled/enablefilters is true.
- if ($this->enablefilters) {
- if (is_array($this->customfilterobjects)) {
- foreach ($this->customfilterobjects as $filterobjects) {
- $this->searchconditions[] = $filterobjects;
+ public function add_standard_search_conditions(): void {
+ foreach ($this->plugins as $componentname => $plugin) {
+ if (\core\plugininfo\qbank::is_plugin_enabled($componentname)) {
+ $pluginentrypointobject = new $plugin();
+ if ($componentname === 'qbank_managecategories') {
+ $pluginentrypointobject = new quiz_managecategories_feature();
}
- } else {
- if ($CFG->usetags) {
- array_unshift($this->searchconditions,
- new \core_question\bank\search\tag_condition([$catcontext, $thiscontext], $tagids));
+ if ($componentname === 'qbank_viewquestiontext' || $componentname === 'qbank_deletequestion') {
+ continue;
+ }
+ $pluginobjects = $pluginentrypointobject->get_question_filters($this);
+ foreach ($pluginobjects as $pluginobject) {
+ $this->add_searchcondition($pluginobject, $pluginobject->get_condition_key());
}
-
- array_unshift($this->searchconditions, new \core_question\bank\search\hidden_condition(!$showhidden));
- array_unshift($this->searchconditions, new custom_category_condition(
- $cat, $recurse, $editcontexts, $this->baseurl, $this->course));
}
}
- $this->display_options_form($showquestiontext);
}
/**
diff --git a/mod/quiz/classes/question/bank/filter/custom_category_condition.php b/mod/quiz/classes/question/bank/filter/custom_category_condition.php
index b3544bbe52d..ba3a5144378 100644
--- a/mod/quiz/classes/question/bank/filter/custom_category_condition.php
+++ b/mod/quiz/classes/question/bank/filter/custom_category_condition.php
@@ -16,6 +16,8 @@
namespace mod_quiz\question\bank\filter;
+use qbank_managecategories\helper;
+
/**
* A custom filter condition for quiz to select question categories.
*
@@ -27,19 +29,23 @@ namespace mod_quiz\question\bank\filter;
* @author Safat Shahin
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-class custom_category_condition extends \core_question\bank\search\category_condition {
+class custom_category_condition extends \qbank_managecategories\category_condition {
- public function display_options() {
- global $PAGE;
- $displaydata = [];
- $catmenu = custom_category_condition_helper::question_category_options($this->contexts, true, 0,
- true, -1, false);
- $displaydata['categoryselect'] = \html_writer::select($catmenu, 'category', $this->cat, [],
- ['class' => 'searchoptions custom-select', 'id' => 'id_selectacategory']);
- $displaydata['categorydesc'] = '';
- if ($this->category) {
- $displaydata['categorydesc'] = $this->print_category_info($this->category);
+ public function get_initial_values() {
+ $catmenu = custom_category_condition_helper::question_category_options($this->contexts, true, 0, true, -1, false);
+ $values = [];
+ foreach ($catmenu as $menu) {
+ foreach ($menu as $catlist) {
+ foreach ($catlist as $key => $value) {
+ $values[] = (object) [
+ // Remove contextid from value.
+ 'value' => strpos($key, ',') === false ? $key : substr($key, 0, strpos($key, ',')),
+ 'title' => $value,
+ 'selected' => ($key === $this->cat),
+ ];
+ }
+ }
}
- return $PAGE->get_renderer('core_question', 'bank')->render_category_condition($displaydata);
+ return $values;
}
}
diff --git a/mod/quiz/classes/question/bank/qbank_helper.php b/mod/quiz/classes/question/bank/qbank_helper.php
index feef74f3660..2f9c05e1847 100644
--- a/mod/quiz/classes/question/bank/qbank_helper.php
+++ b/mod/quiz/classes/question/bank/qbank_helper.php
@@ -18,6 +18,8 @@ namespace mod_quiz\question\bank;
use core_question\local\bank\question_version_status;
use core_question\local\bank\random_question_loader;
+use core_question\question_reference_manager;
+use qbank_tagquestion\tag_condition;
use qubaid_condition;
defined('MOODLE_INTERNAL') || die();
@@ -110,6 +112,7 @@ class qbank_helper {
slot.displaynumber,
slot.requireprevious,
qsr.filtercondition,
+ qsr.usingcontextid,
qv.status,
qv.id AS versionid,
qv.version,
@@ -172,11 +175,12 @@ class qbank_helper {
if ($slot->filtercondition) {
// Unpack the information about a random question.
- $filtercondition = json_decode($slot->filtercondition);
$slot->questionid = 's' . $slot->id; // Sometimes this is used as an array key, so needs to be unique.
- $slot->category = $filtercondition->questioncategoryid;
- $slot->randomrecurse = (bool) $filtercondition->includingsubcategories;
- $slot->randomtags = isset($filtercondition->tags) ? (array) $filtercondition->tags : [];
+ $filter = json_decode($slot->filtercondition, true);
+ $slot->filtercondition = question_reference_manager::convert_legacy_set_reference_filter_condition($filter);
+
+ $slot->category = $slot->filtercondition['filter']['category']['values'][0] ?? 0;
+
$slot->qtype = 'random';
$slot->name = get_string('random', 'quiz');
$slot->length = 1;
@@ -208,9 +212,12 @@ class qbank_helper {
*/
public static function get_tag_ids_for_slot(\stdClass $slotdata): array {
$tagids = [];
- foreach ($slotdata->randomtags as $taginfo) {
- [$id] = explode(',', $taginfo, 2);
- $tagids[] = $id;
+ if (!isset($slotdata->filtercondition['filter'])) {
+ return $tagids;
+ }
+ $filter = $slotdata->filtercondition['filter'];
+ if (isset($filter['qtagids'])) {
+ $tagids = $filter['qtagids']['values'];
}
return $tagids;
}
@@ -222,10 +229,19 @@ class qbank_helper {
* @return string that can be used to display the random slot.
*/
public static function describe_random_question(\stdClass $slotdata): string {
- global $DB;
- $category = $DB->get_record('question_categories', ['id' => $slotdata->category]);
- return \question_bank::get_qtype('random')->question_name(
- $category, $slotdata->randomrecurse, $slotdata->randomtags);
+ $qtagids = self::get_tag_ids_for_slot($slotdata);
+
+ if ($qtagids) {
+ $tagnames = [];
+ $tags = \core_tag_tag::get_bulk($qtagids, 'id, name');
+ foreach ($tags as $tag) {
+ $tagnames[] = $tag->name;
+ }
+ $description = get_string('randomqnametags', 'mod_quiz', implode(",", $tagnames));
+ } else {
+ $description = get_string('randomqname', 'mod_quiz');
+ }
+ return shorten_text($description, 255);
}
/**
@@ -249,8 +265,9 @@ class qbank_helper {
// Random question.
$randomloader = new random_question_loader($qubaids, []);
- $newqusetionid = $randomloader->get_next_question_id($slotdata->category,
- $slotdata->randomrecurse, self::get_tag_ids_for_slot($slotdata));
+ $fitlercondition = $slotdata->filtercondition;
+ $filter = $fitlercondition['filter'] ?? [];
+ $newqusetionid = $randomloader->get_next_filtered_question_id($filter);
if ($newqusetionid === null) {
throw new \moodle_exception('notenoughrandomquestions', 'quiz');
diff --git a/mod/quiz/classes/question/bank/quiz_managecategories_feature.php b/mod/quiz/classes/question/bank/quiz_managecategories_feature.php
new file mode 100644
index 00000000000..6ff8744510f
--- /dev/null
+++ b/mod/quiz/classes/question/bank/quiz_managecategories_feature.php
@@ -0,0 +1,39 @@
+.
+
+namespace mod_quiz\question\bank;
+
+use core_question\local\bank\view;
+use mod_quiz\question\bank\filter\custom_category_condition;
+
+/**
+ * Class quiz_managecategories_feature
+ *
+ * Overrides the default categories feature with a custom category condition.
+ *
+ * @package mod_quiz
+ * @copyright 2022 Catalyst IT EU Ltd.
+ * @author Mark Johnson
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class quiz_managecategories_feature extends \qbank_managecategories\plugin_feature {
+
+ public function get_question_filters(view $qbank = null): array {
+ return [
+ new custom_category_condition($qbank),
+ ];
+ }
+}
diff --git a/mod/quiz/classes/question/bank/random_question_view.php b/mod/quiz/classes/question/bank/random_question_view.php
new file mode 100644
index 00000000000..57bfb70ff6a
--- /dev/null
+++ b/mod/quiz/classes/question/bank/random_question_view.php
@@ -0,0 +1,48 @@
+.
+
+namespace mod_quiz\question\bank;
+
+/**
+ * Subclass to customise the view of the question bank for the quiz editing screen.
+ *
+ * @package mod_quiz
+ * @copyright 2022 Catalyst IT Australia Pty Ltd
+ * @author 2022 Nathan Nguyen
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class random_question_view extends custom_view {
+ /**
+ * Init required columns.
+ *
+ * @return void
+ */
+ protected function init_required_columns(): void {
+ // Override core columns.
+ $this->corequestionbankcolumns = [
+ 'question_type_column',
+ 'question_name_text_column',
+ 'preview_action_column'
+ ];
+ }
+
+ /**
+ * Prints the table of questions in a category with interactions
+ *
+ * @param \context $catcontext*/
+ protected function display_bottom_controls(\context $catcontext): void {
+ }
+}
diff --git a/mod/quiz/classes/quiz_settings.php b/mod/quiz/classes/quiz_settings.php
index 12f2f17528f..d07f0fa2c91 100644
--- a/mod/quiz/classes/quiz_settings.php
+++ b/mod/quiz/classes/quiz_settings.php
@@ -570,12 +570,14 @@ class quiz_settings {
continue;
}
if ($questiondata->qtype === 'random' && $includepotential) {
- if (!isset($qcategories[$questiondata->category])) {
- $qcategories[$questiondata->category] = false;
- }
- if (!empty($questiondata->filtercondition)) {
- $filtercondition = json_decode($questiondata->filtercondition);
- $qcategories[$questiondata->category] = !empty($filtercondition->includingsubcategories);
+ $filtercondition = $questiondata->filtercondition;
+ if (!empty($filtercondition)) {
+ $filter = $filtercondition['filter'];
+ if (isset($filter['category'])) {
+ foreach ($filter['category']['values'] as $catid) {
+ $qcategories[$catid] = $filter['category']['filteroptions']['includesubcategories'];
+ }
+ }
}
} else {
if (!in_array($questiondata->qtype, $questiontypes)) {
diff --git a/mod/quiz/classes/structure.php b/mod/quiz/classes/structure.php
index 8b2f290f927..15002416706 100644
--- a/mod/quiz/classes/structure.php
+++ b/mod/quiz/classes/structure.php
@@ -1389,4 +1389,43 @@ class structure {
// All the associated code for this method have been removed to get rid of accidental call or errors.
return [];
}
+
+ /**
+ * Add a random question to the quiz at a given point.
+ *
+ * @param int $addonpage the page on which to add the question.
+ * @param int $number the number of random questions to add.
+ * @param array $filtercondition the filter condition. Must contain at least a category filter.
+ */
+ public function add_random_questions(int $addonpage, int $number, array $filtercondition): void {
+ global $DB;
+
+ if (!isset($filtercondition['filter']['category'])) {
+ throw new \invalid_parameter_exception('$filtercondition must contain at least a category filter.');
+ }
+ $categoryid = $filtercondition['filter']['category']['values'][0];
+
+ $category = $DB->get_record('question_categories', ['id' => $categoryid]);
+ if (!$category) {
+ new \moodle_exception('invalidcategoryid');
+ }
+
+ $catcontext = \context::instance_by_id($category->contextid);
+ require_capability('moodle/question:useall', $catcontext);
+
+ // Create the selected number of random questions.
+ for ($i = 0; $i < $number; $i++) {
+ // Slot data.
+ $randomslotdata = new stdClass();
+ $randomslotdata->quizid = $this->get_quizid();
+ $randomslotdata->usingcontextid = context_module::instance($this->get_cmid())->id;
+ $randomslotdata->questionscontextid = $category->contextid;
+ $randomslotdata->maxmark = 1;
+
+ $randomslot = new \mod_quiz\local\structure\slot_random($randomslotdata);
+ $randomslot->set_quiz($this->get_quiz());
+ $randomslot->set_filter_condition(json_encode($filtercondition));
+ $randomslot->insert($addonpage);
+ }
+ }
}
diff --git a/mod/quiz/db/services.php b/mod/quiz/db/services.php
index 9cbdb269e4e..2d6545bce49 100644
--- a/mod/quiz/db/services.php
+++ b/mod/quiz/db/services.php
@@ -215,4 +215,20 @@ $functions = [
'capabilities' => 'mod/quiz:reopenattempts',
'ajax' => true,
],
+
+ 'mod_quiz_add_random_questions' => [
+ 'classname' => 'mod_quiz\external\add_random_questions',
+ 'description' => 'Add a number of random questions to a quiz.',
+ 'type' => 'write',
+ 'capabilities' => 'mod/quiz:manage',
+ 'ajax' => true,
+ ],
+
+ 'mod_quiz_update_filter_condition' => [
+ 'classname' => 'mod_quiz\external\update_filter_condition',
+ 'description' => 'Update filter condition for a random question slot.',
+ 'type' => 'write',
+ 'capabilities' => 'mod/quiz:manage',
+ 'ajax' => true,
+ ],
];
diff --git a/mod/quiz/edit.php b/mod/quiz/edit.php
index 2ae72844e4f..1a74a09b494 100644
--- a/mod/quiz/edit.php
+++ b/mod/quiz/edit.php
@@ -49,7 +49,7 @@ require_once($CFG->dirroot . '/question/editlib.php');
$mdlscrollto = optional_param('mdlscrollto', '', PARAM_INT);
list($thispageurl, $contexts, $cmid, $cm, $quiz, $pagevars) =
- question_edit_setup('editq', '/mod/quiz/edit.php', true);
+ question_edit_setup('editq', '/mod/quiz/edit.php', true);
$PAGE->set_url($thispageurl);
$PAGE->set_secondary_active_tab("mod_quiz_edit");
@@ -166,10 +166,6 @@ $event = \mod_quiz\event\edit_page_viewed::create([
]);
$event->trigger();
-// Get the question bank view.
-$questionbank = new mod_quiz\question\bank\custom_view($contexts, $thispageurl, $course, $cm, $quiz);
-$questionbank->set_quiz_has_attempts($quizhasattempts);
-
// End of process commands =====================================================.
$PAGE->set_pagelayout('incourse');
@@ -184,8 +180,13 @@ $node = $PAGE->settingsnav->find('mod_quiz_edit', navigation_node::TYPE_SETTING)
if ($node) {
$node->make_active();
}
-echo $OUTPUT->header();
+// Add random question - result message.
+if ($message = optional_param('message', '', PARAM_TEXT)) {
+ core\notification::add($message, core\notification::SUCCESS);
+}
+
+echo $OUTPUT->header();
// Initialise the JavaScript.
$quizeditconfig = new stdClass();
$quizeditconfig->url = $thispageurl->out(true, ['qbanktool' => '0']);
diff --git a/mod/quiz/editrandom.php b/mod/quiz/editrandom.php
index 155edfb9de0..b0412d16e03 100644
--- a/mod/quiz/editrandom.php
+++ b/mod/quiz/editrandom.php
@@ -24,9 +24,11 @@
*/
use mod_quiz\quiz_settings;
+use mod_quiz\question\bank\random_question_view;
require_once(__DIR__ . '/../../config.php');
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
+require_once($CFG->dirroot . '/mod/quiz/lib.php');
$slotid = required_param('slotid', PARAM_INT);
$returnurl = optional_param('returnurl', '', PARAM_LOCALURL);
@@ -53,16 +55,14 @@ $PAGE->add_body_class('limitedwidth');
$setreference = $DB->get_record('question_set_references',
['itemid' => $slot->id, 'component' => 'mod_quiz', 'questionarea' => 'slot']);
-$filterconditions = json_decode($setreference->filtercondition);
+$filterconditions = json_decode($setreference->filtercondition, true);
-// Validate the question category.
-if (!$category = $DB->get_record('question_categories', ['id' => $filterconditions->questioncategoryid])) {
- new moodle_exception('categorydoesnotexist', 'question', $returnurl);
-}
+$params = $filterconditions;
+$params['cmid'] = $cm->id;
+$extraparams['view'] = random_question_view::class;
-// Check permissions.
-$catcontext = context::instance_by_id($category->contextid);
-require_capability('moodle/question:useall', $catcontext);
+// Build required parameters.
+[$contexts, $thispageurl, $cm, $pagevars, $extraparams] = build_required_parameters_for_custom_view($params, $extraparams);
$thiscontext = $quizobj->get_context();
$contexts = new core_question\local\bank\question_edit_contexts($thiscontext);
@@ -72,11 +72,15 @@ $mform = new mod_quiz\form\randomquestion_form(new moodle_url('/mod/quiz/editran
// Set the form data.
$toform = new stdClass();
-$toform->category = "{$category->id},{$category->contextid}";
-$toform->includesubcategories = $filterconditions->includingsubcategories;
+$toform->category = $filterconditions['filter']['category']['values'][0];
+$includesubcategories = false;
+if (!empty($filterconditions['filter']['category']['filteroptions']['includesubcategories'])) {
+ $includesubcategories = true;
+}
+$toform->includesubcategories = $includesubcategories;
$toform->fromtags = [];
-if (isset($filterconditions->tags)) {
- $currentslottags = $filterconditions->tags;
+if (isset($filterconditions['tags'])) {
+ $currentslottags = $filterconditions['tags'];
foreach ($currentslottags as $slottag) {
$toform->fromtags[] = $slottag;
}
@@ -130,11 +134,22 @@ $PAGE->set_title('Random question');
$PAGE->set_heading($COURSE->fullname);
$PAGE->navbar->add('Random question');
+// Custom View.
+$questionbank = new random_question_view($contexts, $thispageurl, $course, $cm, $params, $extraparams);
+
+// Output.
+$renderer = $PAGE->get_renderer('mod_quiz', 'edit');
+$data = new \stdClass();
+$data->questionbank = $renderer->question_bank_contents($questionbank, $params);
+$data->cmid = $cm->id;
+$data->slotid = $slot->id;
+$data->returnurl = $returnurl;
+$updateform = $OUTPUT->render_from_template('mod_quiz/update_filter_condition_form', $data);
+$PAGE->requires->js_call_amd('mod_quiz/update_random_question_filter_condition', 'init');
+
// Display a heading, question editing form.
echo $OUTPUT->header();
$heading = get_string('randomediting', 'mod_quiz');
echo $OUTPUT->heading_with_help($heading, 'randomquestion', 'mod_quiz');
-
-$mform->display();
-
+echo $updateform;
echo $OUTPUT->footer();
diff --git a/mod/quiz/lang/en/quiz.php b/mod/quiz/lang/en/quiz.php
index e3f0a7b37ba..bc44b1d5428 100644
--- a/mod/quiz/lang/en/quiz.php
+++ b/mod/quiz/lang/en/quiz.php
@@ -32,6 +32,7 @@ $string['addaquestion'] = 'a new question';
$string['addasection'] = 'a new section heading';
$string['addarandomquestion'] = 'a random question';
$string['addarandomquestion_help'] = 'When a random question is added, it results in a randomly-chosen question from the category being inserted into the quiz. This means that different students are likely to get a different selection of questions, and when a quiz allows multiple attempts then each attempt is likely to contain a new selection of questions.';
+$string['addarandomquestion_success'] = 'Random questions have been added';
$string['addarandomselectedquestion'] = 'Add a random selected question ...';
$string['adddescriptionlabel'] = 'Add a description item';
$string['addingquestion'] = 'Adding a question';
@@ -817,6 +818,8 @@ $string['randomfromexistingcategory'] = 'Random question from an existing catego
$string['randomfromunavailabletag'] = '{$a} (unavailable)';
$string['randomnumber'] = 'Number of random questions';
$string['randomnosubcat'] = 'Questions from this category only, not its subcategories.';
+$string['randomqname'] = 'Random question based on filter condition';
+$string['randomqnametags'] = 'Random question based on filter condition with tags: {$a}';
$string['randomquestion'] = 'Random question';
$string['randomquestion_help'] = 'A random question is a way of inserting a randomly-chosen question from a specified category or by a specified tag into an activity.';
$string['randomquestiontags'] = 'Tags';
@@ -1049,6 +1052,8 @@ $string['unfinished'] = 'open';
$string['ungraded'] = 'Ungraded';
$string['unit'] = 'Unit';
$string['unknowntype'] = 'Question type not supported at line {$a}. The question will be ignored';
+$string['updatefilterconditon'] = 'Update filter conditions';
+$string['updatefilterconditon_success'] = 'Successfully updated filter conditions';
$string['updateoverdueattemptstask'] = 'Updating overdue quiz attempts';
$string['updatesettings'] = 'Update quiz settings';
$string['updatequizslotswithrandomxofy'] = 'Updating quiz slots with "random" question data ({$a->done}/{$a->total})';
diff --git a/mod/quiz/lib.php b/mod/quiz/lib.php
index 9213c0781fe..5c3130d6b56 100644
--- a/mod/quiz/lib.php
+++ b/mod/quiz/lib.php
@@ -26,10 +26,11 @@
*/
+use qbank_managecategories\helper;
+
defined('MOODLE_INTERNAL') || die();
use mod_quiz\access_manager;
-use mod_quiz\form\add_random_form;
use mod_quiz\grade_calculator;
use mod_quiz\question\bank\custom_view;
use mod_quiz\question\bank\qbank_helper;
@@ -41,6 +42,7 @@ use mod_quiz\quiz_attempt;
use mod_quiz\quiz_settings;
require_once($CFG->dirroot . '/calendar/lib.php');
+require_once($CFG->dirroot . '/question/editlib.php');
/**#@+
* Option controlling what options are offered on the quiz settings form.
@@ -2392,27 +2394,27 @@ function mod_quiz_core_calendar_event_timestart_updated(\calendar_event $event,
* @param array $args The fragment arguments.
* @return string The rendered mform fragment.
*/
-function mod_quiz_output_fragment_quiz_question_bank($args) {
- global $CFG, $DB, $PAGE;
- require_once($CFG->dirroot . '/mod/quiz/locallib.php');
- require_once($CFG->dirroot . '/question/editlib.php');
+function mod_quiz_output_fragment_quiz_question_bank($args): string {
+ global $PAGE;
- $querystring = preg_replace('/^\?/', '', $args['querystring']);
+ // Retrieve params.
$params = [];
+ $extraparams = [];
+ $querystring = parse_url($args['querystring'], PHP_URL_QUERY);
parse_str($querystring, $params);
- // Build the required resources. The $params are all cleaned as
- // part of this process.
- list($thispageurl, $contexts, $cmid, $cm, $quiz, $pagevars) =
- question_build_edit_resources('editq', '/mod/quiz/edit.php', $params, custom_view::DEFAULT_PAGE_SIZE);
+ $viewclass = \mod_quiz\question\bank\custom_view::class;
+ $extraparams['view'] = $viewclass;
- // Get the course object and related bits.
- $course = get_course($quiz->course);
+ // Build required parameters.
+ [$contexts, $thispageurl, $cm, $pagevars, $extraparams] =
+ build_required_parameters_for_custom_view($params, $extraparams);
+
+ $course = get_course($cm->course);
require_capability('mod/quiz:manage', $contexts->lowest());
- // Create quiz question bank view.
- $questionbank = new custom_view($contexts, $thispageurl, $course, $cm, $quiz);
- $questionbank->set_quiz_has_attempts(quiz_has_attempts($quiz->id));
+ // Custom View.
+ $questionbank = new $viewclass($contexts, $thispageurl, $course, $cm, $pagevars, $extraparams);
// Output.
$renderer = $PAGE->get_renderer('mod_quiz', 'edit');
@@ -2433,32 +2435,54 @@ function mod_quiz_output_fragment_quiz_question_bank($args) {
* @return string The rendered mform fragment.
*/
function mod_quiz_output_fragment_add_random_question_form($args) {
- global $CFG;
+ global $PAGE, $OUTPUT;
- $contexts = new \core_question\local\bank\question_edit_contexts($args['context']);
- $formoptions = [
- 'contexts' => $contexts,
- 'cat' => $args['cat']
- ];
- $formdata = [
- 'category' => $args['cat'],
- 'addonpage' => $args['addonpage'],
- 'returnurl' => $args['returnurl'],
- 'cmid' => $args['cmid']
+ $extraparams = [];
+
+ // Build required parameters.
+ [$contexts, $thispageurl, $cm, $pagevars, $extraparams] =
+ build_required_parameters_for_custom_view($args, $extraparams);
+
+ // Additional param to differentiate with other question bank view.
+ $extraparams['view'] = mod_quiz\question\bank\random_question_view::class;
+
+ $course = get_course($cm->course);
+ require_capability('mod/quiz:manage', $contexts->lowest());
+
+ // Custom View.
+ $questionbank = new mod_quiz\question\bank\random_question_view($contexts, $thispageurl, $course, $cm, $pagevars, $extraparams);
+
+ $renderer = $PAGE->get_renderer('mod_quiz', 'edit');
+ $questionbankoutput = $renderer->question_bank_contents($questionbank, $pagevars);
+
+ $maxrand = 100;
+ for ($i = 1; $i <= min(100, $maxrand); $i++) {
+ $randomcount[] = ['value' => $i, 'name' => $i];
+ }
+
+ // Parent category select.
+ $usablecontexts = $contexts->having_cap('moodle/question:useall');
+ $categoriesarray = helper::question_category_options($usablecontexts);
+ $catoptions = [];
+ foreach ($categoriesarray as $group => $opts) {
+ // Options for each category group.
+ $categories = [];
+ foreach ($opts as $context => $name) {
+ $categories[] = ['value' => $context, 'name' => $name];
+ }
+ $catoptions[] = ['label' => $group, 'options' => $categories];
+ }
+
+ // Template data.
+ $data = [
+ 'questionbank' => $questionbankoutput,
+ 'randomoptions' => $randomcount,
+ 'questioncategoryoptions' => $catoptions,
];
- $form = new add_random_form(
- new \moodle_url('/mod/quiz/addrandom.php'),
- $formoptions,
- 'post',
- '',
- null,
- true,
- $formdata
- );
- $form->set_data($formdata);
+ $result = $OUTPUT->render_from_template('mod_quiz/add_random_question_form', $data);
- return $form->render();
+ return $result;
}
/**
@@ -2505,6 +2529,61 @@ function quiz_delete_references($quizid): void {
}
}
+/**
+ * Question data fragment to get the question html via ajax call.
+ *
+ * @param array $args
+ * @return string
+ */
+function mod_quiz_output_fragment_question_data(array $args): string {
+ // Return if there is no args.
+ if (empty($args)) {
+ return '';
+ }
+
+ // Retrieve params from query string.
+ [$params, $extraparams] = \core_question\local\bank\filter_condition_manager::extract_parameters_from_fragment_args($args);
+
+ // Build required parameters.
+ $cmid = clean_param($args['cmid'], PARAM_INT);
+ $thispageurl = new \moodle_url('/mod/quiz/edit.php', ['cmid' => $cmid]);
+ $thiscontext = \context_module::instance($cmid);
+ $contexts = new \core_question\local\bank\question_edit_contexts($thiscontext);
+ $course = get_course($params['courseid']);
+ [, $cm] = get_module_from_cmid($cmid);
+
+ // Custom question bank View.
+ $viewclass = clean_param($args['view'], PARAM_NOTAGS);
+ $questionbank = new $viewclass($contexts, $thispageurl, $course, $cm, $params, $extraparams);
+
+ // Question table.
+ return $questionbank->display_questions_table();
+}
+
+/**
+ * Build required parameters for question bank custom view
+ *
+ * @param array $params the page parameters
+ * @param array $extraparams additional parameters
+ * @return array
+ */
+function build_required_parameters_for_custom_view(array $params, array $extraparams): array {
+ // Retrieve questions per page.
+ $viewclass = $extraparams['view'] ?? null;
+ $defaultpagesize = $viewclass ? $viewclass::DEFAULT_PAGE_SIZE : DEFAULT_QUESTIONS_PER_PAGE;
+ // Build the required params.
+ [$thispageurl, $contexts, $cmid, $cm, , $pagevars] = question_build_edit_resources(
+ 'editq',
+ '/mod/quiz/edit.php',
+ array_merge($params, $extraparams),
+ $defaultpagesize);
+
+ // Add cmid so we can retrieve later in extra params.
+ $extraparams['cmid'] = $cmid;
+
+ return [$contexts, $thispageurl, $cm, $pagevars, $extraparams];
+}
+
/**
* Implement the calculate_question_stats callback.
*
diff --git a/mod/quiz/locallib.php b/mod/quiz/locallib.php
index 24140d09f20..83636df5476 100644
--- a/mod/quiz/locallib.php
+++ b/mod/quiz/locallib.php
@@ -213,8 +213,9 @@ function quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $time
// Deal with fixed random choices for testing.
if (isset($questionids[$quba->next_slot_number()])) {
- if ($randomloader->is_question_available($questiondata->category,
- (bool) $questiondata->questiontext, $questionids[$quba->next_slot_number()], $tagids)) {
+ $filtercondition = $questiondata->filtercondition;
+ $filters = $filtercondition['filter'] ?? [];
+ if ($randomloader->is_filtered_question_available($filters, $questionids[$quba->next_slot_number()])) {
$questions[$slot] = question_bank::load_question(
$questionids[$quba->next_slot_number()], $quizobj->get_quiz()->shuffleanswers);
continue;
@@ -224,8 +225,10 @@ function quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $time
}
// Normal case, pick one at random.
- $questionid = $randomloader->get_next_question_id($questiondata->category,
- $questiondata->randomrecurse, $tagids);
+ $filtercondition = $questiondata->filtercondition;
+ $filters = $filtercondition['filter'] ?? [];
+ $questionid = $randomloader->get_next_filtered_question_id($filters);
+
if ($questionid === null) {
throw new moodle_exception('notenoughrandomquestions', 'quiz',
$quizobj->view_url(), $questiondata);
@@ -1884,54 +1887,18 @@ function quiz_update_section_firstslots($quizid, $direction, $afterslot, $before
* @param int $addonpage the page on which to add the question.
* @param int $categoryid the question category to add the question from.
* @param int $number the number of random questions to add.
- * @param bool $includesubcategories whether to include questoins from subcategories.
- * @param int[] $tagids Array of tagids. The question that will be picked randomly should be tagged with all these tags.
+ * @deprecated Since Moodle 4.3 MDL-72321
+ * @todo Final deprecation in Moodle 4.7 MDL-78091
*/
-function quiz_add_random_questions($quiz, $addonpage, $categoryid, $number,
- $includesubcategories, $tagids = []) {
- global $DB;
+function quiz_add_random_questions(stdClass $quiz, int $addonpage, int $categoryid, int $number): void {
+ debugging(
+ 'quiz_add_random_questions is deprecated. Please use mod_quiz\structure::add_random_questions() instead.',
+ DEBUG_DEVELOPER
+ );
- $category = $DB->get_record('question_categories', ['id' => $categoryid]);
- if (!$category) {
- new moodle_exception('invalidcategoryid');
- }
-
- $catcontext = context::instance_by_id($category->contextid);
- require_capability('moodle/question:useall', $catcontext);
-
- // Tags for filter condition.
- $tags = \core_tag_tag::get_bulk($tagids, 'id, name');
- $tagstrings = [];
- foreach ($tags as $tag) {
- $tagstrings[] = "{$tag->id},{$tag->name}";
- }
- // Create the selected number of random questions.
- for ($i = 0; $i < $number; $i++) {
- // Set the filter conditions.
- $filtercondition = new stdClass();
- $filtercondition->questioncategoryid = $categoryid;
- $filtercondition->includingsubcategories = $includesubcategories ? 1 : 0;
- if (!empty($tagstrings)) {
- $filtercondition->tags = $tagstrings;
- }
-
- if (!isset($quiz->cmid)) {
- $cm = get_coursemodule_from_instance('quiz', $quiz->id, $quiz->course);
- $quiz->cmid = $cm->id;
- }
-
- // Slot data.
- $randomslotdata = new stdClass();
- $randomslotdata->quizid = $quiz->id;
- $randomslotdata->usingcontextid = context_module::instance($quiz->cmid)->id;
- $randomslotdata->questionscontextid = $category->contextid;
- $randomslotdata->maxmark = 1;
-
- $randomslot = new \mod_quiz\local\structure\slot_random($randomslotdata);
- $randomslot->set_quiz($quiz);
- $randomslot->set_filter_condition($filtercondition);
- $randomslot->insert($addonpage);
- }
+ $settings = quiz_settings::create($quiz->id);
+ $structure = \mod_quiz\structure::create_for_quiz($settings);
+ $structure->add_random_questions($addonpage, $number, $categoryid);
}
/**
diff --git a/mod/quiz/report/overview/tests/behat/regrading.feature b/mod/quiz/report/overview/tests/behat/regrading.feature
index a31927b81e9..8f91fc11cee 100644
--- a/mod/quiz/report/overview/tests/behat/regrading.feature
+++ b/mod/quiz/report/overview/tests/behat/regrading.feature
@@ -197,7 +197,6 @@ Feature: Regrading quiz attempts using the Grades report
And I click on "Yes" "button" in the "Confirm" "dialogue"
And I click on "Add" "link"
And I follow "a random question"
- And I set the field "Number of random questions" to "1"
And I press "Add random question"
And I am on the "Quiz for testing regrading" "quiz activity" page logged in as student3
And I click on "Attempt quiz" "button"
diff --git a/mod/quiz/templates/add_random_question_form.mustache b/mod/quiz/templates/add_random_question_form.mustache
new file mode 100644
index 00000000000..6a95c3c3f97
--- /dev/null
+++ b/mod/quiz/templates/add_random_question_form.mustache
@@ -0,0 +1,90 @@
+{{!
+ 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 .
+}}
+{{!
+ @template mod_quiz/add_random_question_form
+
+ From to add random questions.
+
+ Example context (json):
+ {
+ "returnurl": "http://example.com",
+ "randomoptions": [
+ {"name": "1", "value": "1"},
+ {"name": "2", "value": "2"}
+ ],
+ "questioncategoryoptions": {
+ "label": "Category 1",
+ "options": [
+ {"name": "Category 2", "value": "1"},
+ {"name": "Category 3", "value": "2"}
+ ]
+ },
+ "questionbank": "Question bank HTML goes here "
+ }
+}}
+
+
+
diff --git a/mod/quiz/templates/random_question_form_preview.mustache b/mod/quiz/templates/random_question_container.mustache
similarity index 53%
rename from mod/quiz/templates/random_question_form_preview.mustache
rename to mod/quiz/templates/random_question_container.mustache
index f90ec9f0ec1..18b768c7876 100644
--- a/mod/quiz/templates/random_question_form_preview.mustache
+++ b/mod/quiz/templates/random_question_container.mustache
@@ -15,26 +15,14 @@
along with Moodle. If not, see .
}}
{{!
- @template mod_quiz/random_question_form_preview
+ @template mod_quiz/random_question_container
- A preview section for the list of questions that match the random
- question criteria in the random question form.
-
- Classes required for JS:
- * none
-
- Data attributes required for JS:
- * none
-
- Context variables required for this template:
- * title A cleaned string (use clean_text()) to display.
- * body HTML content for the boday
+ Container for list of filtered questions.
Example context (json):
- {}
+ {
+ }
+
}}
-
-
-
- {{> core/overlay_loading}}
+
diff --git a/mod/quiz/templates/update_filter_condition_form.mustache b/mod/quiz/templates/update_filter_condition_form.mustache
new file mode 100644
index 00000000000..efe499605bb
--- /dev/null
+++ b/mod/quiz/templates/update_filter_condition_form.mustache
@@ -0,0 +1,43 @@
+{{!
+ 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
.
+}}
+{{!
+ @template mod_quiz/update_filter_condition_form
+
+ From to add random questions.
+
+ Example context (json):
+ {
+ "questionbank": "
Question bank HTML goes here ",
+ "returnurl": "https://example.com",
+ "cmid": "1234",
+ "slotid": "5678"
+ }
+}}
+
+
+
+ {{{questionbank}}}
+
+
+
+
+
+
diff --git a/mod/quiz/tests/attempt_walkthrough_from_csv_test.php b/mod/quiz/tests/attempt_walkthrough_from_csv_test.php
index 86a801d442c..4cb2e15304b 100644
--- a/mod/quiz/tests/attempt_walkthrough_from_csv_test.php
+++ b/mod/quiz/tests/attempt_walkthrough_from_csv_test.php
@@ -23,6 +23,7 @@ defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
+require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
/**
* Quiz attempt walk through using data from csv file.
@@ -35,6 +36,8 @@ require_once($CFG->dirroot . '/mod/quiz/locallib.php');
*/
class attempt_walkthrough_from_csv_test extends \advanced_testcase {
+ use \quiz_question_helper_test_trait;
+
/**
* @var string[] names of the files which contain the test data.
*/
@@ -134,7 +137,7 @@ class attempt_walkthrough_from_csv_test extends \advanced_testcase {
if ($slotquestion['type'] !== 'random') {
quiz_add_quiz_question($slotquestion['id'], $this->quiz, 0, $slotquestion['mark']);
} else {
- quiz_add_random_questions($this->quiz, 0, $slotquestion['catid'], 1, 0);
+ $this->add_random_questions($this->quiz->id, 0, $slotquestion['catid'], 1);
$this->randqids[$slotno] = $qidsbycat[$slotquestion['catid']];
}
}
diff --git a/mod/quiz/tests/attempt_walkthrough_test.php b/mod/quiz/tests/attempt_walkthrough_test.php
index c2a294f33ff..21a2c557260 100644
--- a/mod/quiz/tests/attempt_walkthrough_test.php
+++ b/mod/quiz/tests/attempt_walkthrough_test.php
@@ -24,6 +24,7 @@ defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
+require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
/**
* Quiz attempt walk through.
@@ -37,6 +38,8 @@ require_once($CFG->dirroot . '/mod/quiz/locallib.php');
*/
class attempt_walkthrough_test extends \advanced_testcase {
+ use \quiz_question_helper_test_trait;
+
/**
* Create a quiz with questions and walk through a quiz attempt.
*/
@@ -288,7 +291,7 @@ class attempt_walkthrough_test extends \advanced_testcase {
$numq = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]);
// Add random question to the quiz.
- quiz_add_random_questions($quiz, 0, $cat->id, 1, false);
+ $this->add_random_questions($quiz->id, 0, $cat->id, 1);
// Make another category.
$cat2 = $questiongenerator->create_question_category();
diff --git a/mod/quiz/tests/behat/behat_mod_quiz.php b/mod/quiz/tests/behat/behat_mod_quiz.php
index e249c07e26c..ed65cd9aa4e 100644
--- a/mod/quiz/tests/behat/behat_mod_quiz.php
+++ b/mod/quiz/tests/behat/behat_mod_quiz.php
@@ -276,7 +276,18 @@ class behat_mod_quiz extends behat_question_base {
} else {
$includingsubcategories = clean_param($questiondata['includingsubcategories'], PARAM_BOOL);
}
- quiz_add_random_questions($quiz, $page, $question->category, 1, $includingsubcategories);
+
+ $filter = [
+ 'category' => [
+ 'jointype' => \qbank_managecategories\category_condition::JOINTYPE_DEFAULT,
+ 'values' => [$question->category],
+ 'filteroptions' => ['includesubcategories' => $includingsubcategories],
+ ],
+ ];
+ $filtercondition['filter'] = $filter;
+ $settings = quiz_settings::create($quiz->id);
+ $structure = \mod_quiz\structure::create_for_quiz($settings);
+ $structure->add_random_questions($page, 1, $filtercondition);
} else {
// Add the question.
quiz_add_quiz_question($question->id, $quiz, $page, $maxmark);
diff --git a/mod/quiz/tests/behat/editing_add.feature b/mod/quiz/tests/behat/editing_add.feature
index 7fca18fe8bf..639ba3bebb8 100644
--- a/mod/quiz/tests/behat/editing_add.feature
+++ b/mod/quiz/tests/behat/editing_add.feature
@@ -117,7 +117,6 @@ Feature: Edit quiz page - adding things
And I select "Questions" from the "Question bank tertiary navigation" singleselect
And I should see "Question bank"
- And I should see "Select a category"
# Create the Essay 01 question.
When I press "Create a new question ..."
@@ -131,8 +130,6 @@ Feature: Edit quiz page - adding things
And I should see "Essay 01"
# Create the Essay 02 question.
- And I should see "Select a category"
- And I set the field "Select a category:" to "Subcat 1"
When I press "Create a new question ..."
And I set the field "item_qtype_essay" to "1"
And I click on "Add" "button" in the "Choose a question type to add" "dialogue"
@@ -144,7 +141,6 @@ Feature: Edit quiz page - adding things
And I should see "Essay 02"
# Create the Essay 03 question.
- And I set the field "Select a category" to "Default for C1"
And I wait until the page is ready
When I press "Create a new question ..."
And I set the field "item_qtype_essay" to "1"
@@ -201,8 +197,6 @@ Feature: Edit quiz page - adding things
# Add Esay 02 from question bank.
And I open the "Page 1" add to quiz menu
And I follow "from question bank"
- And I should see "Select a category"
- And I set the field "Select a category" to "Subcat 1"
And I click on "Add to quiz" "link" in the "Essay 02" "table_row"
And I should see "Essay 03" on quiz page "1"
And I should see "Essay 01" on quiz page "1"
diff --git a/mod/quiz/tests/behat/editing_add_from_question_bank.feature b/mod/quiz/tests/behat/editing_add_from_question_bank.feature
index 7630ad2e845..a4140863be7 100644
--- a/mod/quiz/tests/behat/editing_add_from_question_bank.feature
+++ b/mod/quiz/tests/behat/editing_add_from_question_bank.feature
@@ -40,8 +40,7 @@ Feature: Adding questions to a quiz from the question bank
Then I should see "foo" in the "question 01 name" "table_row"
And I should see "bar" in the "question 02 name" "table_row"
And I should see "qidnum" in the "question 02 name" "table_row"
- And I set the field "Filter by tags..." to "foo"
- And I press the enter key
+ When I apply question bank filter "Tag" with value "foo"
And I should see "question 01 name" in the "categoryquestions" "table"
And I should not see "question 02 name" in the "categoryquestions" "table"
@@ -76,9 +75,8 @@ Feature: Adding questions to a quiz from the question bank
Then I should see "question 21 name" in the "categoryquestions" "table"
And I should see "question 22 name" in the "categoryquestions" "table"
And I should not see "question 01 name" in the "categoryquestions" "table"
- And I click on "Show all 22" "link" in the ".question-showall-text" "css_element"
+ And I click on "1" "link" in the ".pagination" "css_element"
And I should see "question 01 name" in the "categoryquestions" "table"
- And I should see "question 22 name" in the "categoryquestions" "table"
Scenario: Questions are added in the right place with multiple sections
Given the following "questions" exist:
diff --git a/mod/quiz/tests/behat/editing_add_random.feature b/mod/quiz/tests/behat/editing_add_random.feature
index 17113976a2a..856db6e3c4c 100644
--- a/mod/quiz/tests/behat/editing_add_random.feature
+++ b/mod/quiz/tests/behat/editing_add_random.feature
@@ -43,7 +43,9 @@ Feature: Adding random questions to a quiz based on category and tags
Given I am on the "Quiz 1" "mod_quiz > Edit" page logged in as "teacher1"
When I open the "last" add to quiz menu
And I follow "a random question"
- And I open the autocomplete suggestions list
+ And I add question bank filter "Tag"
+ And I click on "Tag" "field"
+ And I press the down key
Then "foo" "autocomplete_suggestions" should exist
And "bar" "autocomplete_suggestions" should exist
@@ -51,21 +53,16 @@ Feature: Adding random questions to a quiz based on category and tags
Given I am on the "Quiz 1" "mod_quiz > Edit" page logged in as "teacher1"
When I open the "last" add to quiz menu
And I follow "a random question"
- And I set the field "Category" to "Top for Course 1"
- And I wait until the page is ready
- And I open the autocomplete suggestions list
- And I click on "foo" item in the autocomplete list
- Then I should see "question 1 name"
- And I should see "question 3 name"
- And I should not see "question 2 name"
- And I should not see "question 4 name"
- And I set the field "Category" to "Questions Category 1"
+ And I apply question bank filter "Category" with value "Questions Category 1"
+ And I apply question bank filter "Tag" with value "foo"
+ And I click on "Apply filters" "button"
And I wait until the page is ready
And I should see "question 1 name"
And I should not see "question 3 name"
And I should not see "question 2 name"
And I should not see "question 4 name"
- And I click on "Include questions from subcategories too" "checkbox"
+ And I set the field "Also show questions from subcategories" to "1"
+ And I click on "Apply filters" "button"
And I wait until the page is ready
And I should see "question 1 name"
And I should see "question 3 name"
@@ -76,10 +73,11 @@ Feature: Adding random questions to a quiz based on category and tags
Given I am on the "Quiz 1" "mod_quiz > Edit" page logged in as "teacher1"
When I open the "last" add to quiz menu
And I follow "a random question"
- And I set the field "Tags" to "foo"
+ And I apply question bank filter "Tag" with value "foo"
+ And I select "1" from the "randomcount" singleselect
And I press "Add random question"
- And I should see "Random (Questions Category 1, tags: foo)" on quiz page "1"
- And I click on "(See questions)" "link"
+ And I should see "Random question based on filter condition with tags: foo" on quiz page "1"
+ When I click on "Configure question" "link" in the "Random question based on filter condition with tags: foo" "list_item"
Then I should see "Questions Category 1"
And I should see "foo"
And I should see "question 1 name"
@@ -101,8 +99,8 @@ Feature: Adding random questions to a quiz based on category and tags
And I follow "New category"
And I set the following fields to these values:
| Name | New Random category |
- | Parent category | Top for Quiz 1 |
+ | Parent category | Default for Quiz 1 |
And I press "Create category and add random question"
- And I should see "Random (New Random category)" on quiz page "1"
- And I click on "(See questions)" "link"
- Then I should see "Top for Quiz 1"
+ And I should see "Random question based on filter condition" on quiz page "1"
+ And I click on "Configure question" "link" in the "Random question based on filter condition" "list_item"
+ Then I should see "New Random category"
diff --git a/mod/quiz/tests/behat/editing_edit_random.feature b/mod/quiz/tests/behat/editing_edit_random.feature
index d853b4ee0de..df05f8344a6 100644
--- a/mod/quiz/tests/behat/editing_edit_random.feature
+++ b/mod/quiz/tests/behat/editing_edit_random.feature
@@ -38,18 +38,17 @@ Feature: Editing random questions already in a quiz based on category and tags
And I follow "a random question"
# To actually reproduce MDL-68733 it would be better to set tags easy,essay here, and then below just delete one tag.
# However, the state of Behat for autocomplete fields does not let us actually do that.
- And I set the field "Tags" to "easy"
+ And I apply question bank filter "Tag" with value "easy"
And I press "Add random question"
And I open the "Page 1" add to quiz menu
And I follow "a random question"
- And I set the field "Tags" to "hard"
+ And I apply question bank filter "Tag" with value "hard"
And I press "Add random question"
And I follow "Add page break"
- When I click on "Configure question" "link" in the "Random (Questions Category 1, tags: easy)" "list_item"
- And I click on "easy" "autocomplete_selection"
- And I set the field "Tags" to "essay"
- And I press "Save changes"
- Then I should see "Random (Questions Category 1, tags: essay)" on quiz page "1"
- And I should see "Random (Questions Category 1, tags: hard)" on quiz page "2"
- And I click on "Configure question" "link" in the "Questions Category 1, tags: hard" "list_item"
+ When I click on "Configure question" "link" in the "Random question based on filter condition with tags: easy" "list_item"
+ And I apply question bank filter "Tag" with value "essay"
+ And I press "Update filter conditions"
+ Then I should see "Random question based on filter condition with tags: essay" on quiz page "1"
+ And I should see "Random question based on filter condition with tags: hard" on quiz page "2"
+ And I click on "Configure question" "link" in the "Random question based on filter condition with tags: hard" "list_item"
And "hard" "autocomplete_selection" should be visible
diff --git a/mod/quiz/tests/behat/editing_remove_multiple_questions.feature b/mod/quiz/tests/behat/editing_remove_multiple_questions.feature
index 1e7ff95ad03..5aa2fa6adb9 100644
--- a/mod/quiz/tests/behat/editing_remove_multiple_questions.feature
+++ b/mod/quiz/tests/behat/editing_remove_multiple_questions.feature
@@ -261,11 +261,9 @@ Feature: Edit quiz page - remove multiple questions
And I click on "selectquestion-2" "checkbox"
And I click on "Delete selected" "button"
And I click on "Yes" "button" in the "Confirm" "dialogue"
- # To make sure question is deleted completely.
- And I reload the page
- Then I should see "Random (Test questions)" on quiz page "1"
- And I should not see "Random (Test questions)" on quiz page "2"
- And I should not see "Random (Test questions)" on quiz page "3"
+ Then I should see "Random question based on filter condition" on quiz page "1"
+ And I should not see "Random question based on filter condition" on quiz page "2"
+ And I should not see "Random question based on filter condition" on quiz page "3"
And I should see "Total of marks: 1.00"
And I should see "Questions: 1"
@@ -284,16 +282,15 @@ Feature: Edit quiz page - remove multiple questions
# Delete all questions in page. Page contains multiple questions.
When I open the "last" add to quiz menu
And I follow "a random question"
- And I set the field "Number of random questions" to "3"
+ And I click on "menurandomcount" "select"
+ And I click on "3" "option"
And I press "Add random question"
And I click on "Select multiple items" "button"
And I press "Select all"
And I click on "Delete selected" "button"
And I click on "Yes" "button" in the "Confirm" "dialogue"
- # To make sure question is deleted completely.
- And I reload the page
- Then I should not see "Random (Test questions)" on quiz page "1"
- And I should not see "Random (Test questions)" on quiz page "2"
- And I should not see "Random (Test questions)" on quiz page "3"
+ Then I should not see "Random question based on filter condition" on quiz page "1"
+ And I should not see "Random question based on filter condition" on quiz page "2"
+ And I should not see "Random question based on filter condition" on quiz page "3"
And I should see "Total of marks: 0.00"
And I should see "Questions: 0"
diff --git a/mod/quiz/tests/behat/random_question.feature b/mod/quiz/tests/behat/random_question.feature
index a6f3562640d..15a7e047b68 100644
--- a/mod/quiz/tests/behat/random_question.feature
+++ b/mod/quiz/tests/behat/random_question.feature
@@ -33,23 +33,24 @@ Feature: Moving a question to another category should not affect random question
Given I am on the "Quiz 1" "mod_quiz > Edit" page logged in as "teacher1"
When I open the "last" add to quiz menu
And I follow "a random question"
- And I set the field "Category" to "Used category"
+ And I apply question bank filter "Category" with value "Used category"
And I press "Add random question"
- And I should see "Random (Used category)" on quiz page "1"
- And I click on "(See questions)" "link"
+ And I should see "Random question based on filter condition" on quiz page "1"
+ And I click on "Configure question" "link" in the "Random question based on filter condition" "list_item"
And I should see "Used category"
+ And I am on "Course 1" course homepage
+ And I navigate to "Question bank" in current page administration
+ And I apply question bank filter "Category" with value "Used category"
And I click on "Test question to be moved" "checkbox" in the "Test question to be moved" "table_row"
And I click on "With selected" "button"
And I click on question bulk action "move"
And I set the field "Question category" to "Subcategory"
And I press "Move to"
Then I should see "Test question to be moved"
- And the field "Select a category" matches value " Subcategory (1)"
- And the "Select a category" select box should contain "Used category"
- And the "Select a category" select box should not contain "Used category (1)"
+ And I should see "Subcategory (1)"
And I am on the "Quiz 1" "mod_quiz > Edit" page
- And I should see "Random (Used category)" on quiz page "1"
- And I click on "(See questions)" "link"
+ And I should see "Random question based on filter condition" on quiz page "1"
+ And I click on "Configure question" "link" in the "Random question based on filter condition" "list_item"
And I should see "Used category"
@javascript
@@ -57,9 +58,9 @@ Feature: Moving a question to another category should not affect random question
Given I am on the "Quiz 1" "mod_quiz > Edit" page logged in as "teacher1"
When I open the "last" add to quiz menu
And I follow "a random question"
- And I set the field "Category" to "Used category"
+ And I apply question bank filter "Category" with value "Used category"
And I press "Add random question"
- And I should see "Random (Used category)" on quiz page "1"
+ And I should see "Random question based on filter condition" on quiz page "1"
And I am on the "Course 1" "core_question > course question categories" page
And I click on "Edit this category" "link" in the "Used category" "list_item"
And I set the following fields to these values:
@@ -69,4 +70,4 @@ Feature: Moving a question to another category should not affect random question
Then I should see "Used category new"
And I should see "I was edited" in the "Used category new" "list_item"
And I am on the "Quiz 1" "mod_quiz > Edit" page
- And I should see "Random (Used category new)" on quiz page "1"
+ And I should see "Random question based on filter condition" on quiz page "1"
diff --git a/mod/quiz/tests/external/external_test.php b/mod/quiz/tests/external/external_test.php
index 9fc37410f38..d3796d5fbf7 100644
--- a/mod/quiz/tests/external/external_test.php
+++ b/mod/quiz/tests/external/external_test.php
@@ -41,6 +41,7 @@ defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
+require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
/**
* Silly class to access mod_quiz_external internal methods.
@@ -86,6 +87,8 @@ class testable_mod_quiz_external extends mod_quiz_external {
*/
class external_test extends externallib_advanced_testcase {
+ use \quiz_question_helper_test_trait;
+
/** @var \stdClass course record. */
protected $course;
@@ -1898,7 +1901,7 @@ class external_test extends externallib_advanced_testcase {
$question = $questiongenerator->create_question('truefalse', null, ['category' => $cat->id]);
$question = $questiongenerator->create_question('essay', null, ['category' => $cat->id]);
- quiz_add_random_questions($quiz, 0, $cat->id, 1, false);
+ $this->add_random_questions($quiz->id, 0, $cat->id, 1);
$quizobj = quiz_settings::create($quiz->id, $this->student->id);
@@ -2024,8 +2027,8 @@ class external_test extends externallib_advanced_testcase {
$question = $questiongenerator->create_question('essay', null, ['category' => $anothercat->id]);
// Add a couple of random questions from the same category.
- quiz_add_random_questions($quiz, 0, $cat->id, 1, false);
- quiz_add_random_questions($quiz, 0, $cat->id, 1, false);
+ $this->add_random_questions($quiz->id, 0, $cat->id, 1);
+ $this->add_random_questions($quiz->id, 0, $cat->id, 1);
$this->setUser($this->student);
@@ -2039,7 +2042,7 @@ class external_test extends externallib_advanced_testcase {
// Add more questions to the quiz, this time from the other category.
$this->setAdminUser();
- quiz_add_random_questions($quiz, 0, $anothercat->id, 1, false);
+ $this->add_random_questions($quiz->id, 0, $anothercat->id, 1);
$this->setUser($this->student);
$result = mod_quiz_external::get_quiz_required_qtypes($quiz->id);
diff --git a/mod/quiz/tests/fixtures/moodle_42_random_question.mbz b/mod/quiz/tests/fixtures/moodle_42_random_question.mbz
new file mode 100644
index 00000000000..f7a0dcef055
Binary files /dev/null and b/mod/quiz/tests/fixtures/moodle_42_random_question.mbz differ
diff --git a/mod/quiz/tests/lib_test.php b/mod/quiz/tests/lib_test.php
index 53b598a0b0d..7a8bf421408 100644
--- a/mod/quiz/tests/lib_test.php
+++ b/mod/quiz/tests/lib_test.php
@@ -31,12 +31,15 @@ defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/quiz/lib.php');
+require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
/**
* @copyright 2008 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
*/
class lib_test extends \advanced_testcase {
+ use \quiz_question_helper_test_trait;
+
public function test_quiz_has_grades() {
$quiz = new \stdClass();
$quiz->grade = '100.0000';
@@ -109,7 +112,7 @@ class lib_test extends \advanced_testcase {
$standardq = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
quiz_add_quiz_question($standardq->id, $quiz);
- quiz_add_random_questions($quiz, 0, $cat->id, 1, false);
+ $this->add_random_questions($quiz->id, 0, $cat->id, 1);
// Get the random question.
$randomq = $DB->get_record('question', ['qtype' => 'random']);
diff --git a/mod/quiz/tests/local_structure_slot_random_test.php b/mod/quiz/tests/local_structure_slot_random_test.php
index 6c4c11eecac..3a29b7e5757 100644
--- a/mod/quiz/tests/local_structure_slot_random_test.php
+++ b/mod/quiz/tests/local_structure_slot_random_test.php
@@ -16,8 +16,14 @@
namespace mod_quiz;
+defined('MOODLE_INTERNAL') || die();
+
use mod_quiz\question\bank\qbank_helper;
+global $CFG;
+require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
+require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
+
/**
* Class mod_quiz_local_structure_slot_random_test
* Class for tests related to the {@link \mod_quiz\local\structure\slot_random} class.
@@ -26,8 +32,12 @@ use mod_quiz\question\bank\qbank_helper;
* @category test
* @copyright 2018 Shamim Rezaie
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @covers \mod_quiz\local\structure\slot_random
*/
class local_structure_slot_random_test extends \advanced_testcase {
+
+ use \quiz_question_helper_test_trait;
+
/**
* Constructor test.
*/
@@ -57,8 +67,7 @@ class local_structure_slot_random_test extends \advanced_testcase {
// Set the filter conditions.
$filtercondition = new \stdClass();
- $filtercondition->questioncategoryid = $category->id;
- $filtercondition->includingsubcategories = 1;
+ $filtercondition->filters = \question_filter_test_helper::create_filters([$category->id], true);
// Slot data.
$randomslotdata = new \stdClass();
@@ -69,7 +78,7 @@ class local_structure_slot_random_test extends \advanced_testcase {
// Insert the random question to the quiz.
$randomslot = new \mod_quiz\local\structure\slot_random($randomslotdata);
- $randomslot->set_filter_condition($filtercondition);
+ $randomslot->set_filter_condition(json_encode($filtercondition));
$rc = new \ReflectionClass('\mod_quiz\local\structure\slot_random');
$rcp = $rc->getProperty('filtercondition');
@@ -77,8 +86,8 @@ class local_structure_slot_random_test extends \advanced_testcase {
$record = json_decode($rcp->getValue($randomslot));
$this->assertEquals($quiz->id, $randomslot->get_quiz()->id);
- $this->assertEquals($category->id, $record->questioncategoryid);
- $this->assertEquals(1, $record->includingsubcategories);
+ $this->assertEquals($category->id, $record->filters->category->values[0]);
+ $this->assertTrue($record->filters->category->filteroptions->includesubcategories);
$rcp = $rc->getProperty('record');
$rcp->setAccessible(true);
@@ -100,12 +109,11 @@ class local_structure_slot_random_test extends \advanced_testcase {
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$category = $questiongenerator->create_question_category();
- quiz_add_random_questions($quiz, 0, $category->id, 1, false);
+ $this->add_random_questions($quiz->id, 0, $category->id, 1);
// Set the filter conditions.
$filtercondition = new \stdClass();
- $filtercondition->questioncategoryid = $category->id;
- $filtercondition->includingsubcategories = 1;
+ $filtercondition->filters = \question_filter_test_helper::create_filters([$category->id], 1);
// Slot data.
$randomslotdata = new \stdClass();
@@ -115,7 +123,7 @@ class local_structure_slot_random_test extends \advanced_testcase {
$randomslotdata->questionscontextid = $category->contextid;
$randomslot = new \mod_quiz\local\structure\slot_random($randomslotdata);
- $randomslot->set_filter_condition($filtercondition);
+ $randomslot->set_filter_condition(json_encode($filtercondition));
// The create_instance had injected an additional cmid propery to the quiz. Let's remove that.
unset($quiz->cmid);
@@ -137,12 +145,11 @@ class local_structure_slot_random_test extends \advanced_testcase {
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$category = $questiongenerator->create_question_category();
- quiz_add_random_questions($quiz, 0, $category->id, 1, false);
+ $this->add_random_questions($quiz->id, 0, $category->id, 1);
// Set the filter conditions.
$filtercondition = new \stdClass();
- $filtercondition->questioncategoryid = $category->id;
- $filtercondition->includingsubcategories = 1;
+ $filtercondition->filters = \question_filter_test_helper::create_filters([$category->id], 1);
// Slot data.
$randomslotdata = new \stdClass();
@@ -152,7 +159,7 @@ class local_structure_slot_random_test extends \advanced_testcase {
$randomslotdata->questionscontextid = $category->contextid;
$randomslot = new \mod_quiz\local\structure\slot_random($randomslotdata);
- $randomslot->set_filter_condition($filtercondition);
+ $randomslot->set_filter_condition(json_encode($filtercondition));
// The create_instance had injected an additional cmid propery to the quiz. Let's remove that.
unset($quiz->cmid);
@@ -178,7 +185,7 @@ class local_structure_slot_random_test extends \advanced_testcase {
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$category = $questiongenerator->create_question_category();
- quiz_add_random_questions($quiz, 0, $category->id, 1, false);
+ $this->add_random_questions($quiz->id, 0, $category->id, 1);
// Slot data.
$randomslotdata = new \stdClass();
@@ -203,147 +210,24 @@ class local_structure_slot_random_test extends \advanced_testcase {
return [$randomslot, $tags];
}
- public function test_set_tags() {
+ public function test_set_tags_filter() {
$this->resetAfterTest();
$this->setAdminUser();
list($randomslot, $tags) = $this->setup_for_test_tags(['foo', 'bar']);
+
+ $qtagids = [$tags['foo']->id, $tags['bar']->id];
$filtercondition = new \stdClass();
- $randomslot->set_tags([$tags['foo'], $tags['bar']]);
- $randomslot->set_filter_condition($filtercondition);
+ $filtercondition->filters = \question_filter_test_helper::create_filters([], 0, $qtagids);
+ $randomslot->set_filter_condition(json_encode($filtercondition));
$rc = new \ReflectionClass('\mod_quiz\local\structure\slot_random');
$rcp = $rc->getProperty('filtercondition');
$rcp->setAccessible(true);
$tagspropery = $rcp->getValue($randomslot);
- $this->assertEquals([
- $tags['foo']->id => $tags['foo'],
- $tags['bar']->id => $tags['bar'],
- ], (array)json_decode($tagspropery)->tags);
- }
-
- public function test_set_tags_twice() {
- $this->resetAfterTest();
- $this->setAdminUser();
-
- list($randomslot, $tags) = $this->setup_for_test_tags(['foo', 'bar', 'baz']);
-
- // Set tags for the first time.
- $filtercondition = new \stdClass();
- $randomslot->set_tags([$tags['foo'], $tags['bar']]);
- // Now set the tags again.
- $randomslot->set_tags([$tags['baz']]);
- $randomslot->set_filter_condition($filtercondition);
-
- $rc = new \ReflectionClass('\mod_quiz\local\structure\slot_random');
- $rcp = $rc->getProperty('filtercondition');
- $rcp->setAccessible(true);
- $tagspropery = $rcp->getValue($randomslot);
-
- $this->assertEquals([
- $tags['baz']->id => $tags['baz'],
- ], (array)json_decode($tagspropery)->tags);
- }
-
- public function test_set_tags_duplicates() {
- $this->resetAfterTest();
- $this->setAdminUser();
-
- list($randomslot, $tags) = $this->setup_for_test_tags(['foo', 'bar', 'baz']);
- $filtercondition = new \stdClass();
- $randomslot->set_tags([$tags['foo'], $tags['bar'], $tags['foo']]);
- $randomslot->set_filter_condition($filtercondition);
-
- $rc = new \ReflectionClass('\mod_quiz\local\structure\slot_random');
- $rcp = $rc->getProperty('filtercondition');
- $rcp->setAccessible(true);
- $tagspropery = $rcp->getValue($randomslot);
-
- $this->assertEquals([
- $tags['foo']->id => $tags['foo'],
- $tags['bar']->id => $tags['bar'],
- ], (array)json_decode($tagspropery)->tags);
- }
-
- public function test_set_tags_by_id() {
- $this->resetAfterTest();
- $this->setAdminUser();
-
- list($randomslot, $tags) = $this->setup_for_test_tags(['foo', 'bar', 'baz']);
- $filtercondition = new \stdClass();
- $randomslot->set_tags_by_id([$tags['foo']->id, $tags['bar']->id]);
- $randomslot->set_filter_condition($filtercondition);
-
- $rc = new \ReflectionClass('\mod_quiz\local\structure\slot_random');
- $rcp = $rc->getProperty('tags');
- $rcp->setAccessible(true);
- $tagspropery = $rcp->getValue($randomslot);
-
- // The set_tags_by_id function only retrieves id and name fields of the tag object.
- $this->assertCount(2, $tagspropery);
- $this->assertArrayHasKey($tags['foo']->id, $tagspropery);
- $this->assertArrayHasKey($tags['bar']->id, $tagspropery);
- $this->assertEquals(
- (object)['id' => $tags['foo']->id, 'name' => $tags['foo']->name],
- $tagspropery[$tags['foo']->id]->to_object()
- );
- $this->assertEquals(
- (object)['id' => $tags['bar']->id, 'name' => $tags['bar']->name],
- $tagspropery[$tags['bar']->id]->to_object()
- );
- }
-
- public function test_set_tags_by_id_twice() {
- $this->resetAfterTest();
- $this->setAdminUser();
-
- list($randomslot, $tags) = $this->setup_for_test_tags(['foo', 'bar', 'baz']);
-
- // Set tags for the first time.
- $randomslot->set_tags_by_id([$tags['foo']->id, $tags['bar']->id]);
- // Now set the tags again.
- $randomslot->set_tags_by_id([$tags['baz']->id]);
-
- $rc = new \ReflectionClass('\mod_quiz\local\structure\slot_random');
- $rcp = $rc->getProperty('tags');
- $rcp->setAccessible(true);
- $tagspropery = $rcp->getValue($randomslot);
-
- // The set_tags_by_id function only retrieves id and name fields of the tag object.
- $this->assertCount(1, $tagspropery);
- $this->assertArrayHasKey($tags['baz']->id, $tagspropery);
- $this->assertEquals(
- (object)['id' => $tags['baz']->id, 'name' => $tags['baz']->name],
- $tagspropery[$tags['baz']->id]->to_object()
- );
- }
-
- public function test_set_tags_by_id_duplicates() {
- $this->resetAfterTest();
- $this->setAdminUser();
-
- list($randomslot, $tags) = $this->setup_for_test_tags(['foo', 'bar', 'baz']);
-
- $randomslot->set_tags_by_id([$tags['foo']->id, $tags['bar']->id], $tags['foo']->id);
-
- $rc = new \ReflectionClass('\mod_quiz\local\structure\slot_random');
- $rcp = $rc->getProperty('tags');
- $rcp->setAccessible(true);
- $tagspropery = $rcp->getValue($randomslot);
-
- // The set_tags_by_id function only retrieves id and name fields of the tag object.
- $this->assertCount(2, $tagspropery);
- $this->assertArrayHasKey($tags['foo']->id, $tagspropery);
- $this->assertArrayHasKey($tags['bar']->id, $tagspropery);
- $this->assertEquals(
- (object)['id' => $tags['foo']->id, 'name' => $tags['foo']->name],
- $tagspropery[$tags['foo']->id]->to_object()
- );
- $this->assertEquals(
- (object)['id' => $tags['bar']->id, 'name' => $tags['bar']->name],
- $tagspropery[$tags['bar']->id]->to_object()
- );
+ $this->assertEquals([$tags['foo']->id, $tags['bar']->id],
+ (array)json_decode($tagspropery)->filters->qtagids->values);
}
public function test_insert() {
@@ -389,8 +273,7 @@ class local_structure_slot_random_test extends \advanced_testcase {
// Set the filter conditions.
$filtercondition = new \stdClass();
- $filtercondition->questioncategoryid = $category->id;
- $filtercondition->includingsubcategories = 1;
+ $filtercondition->filter = \question_filter_test_helper::create_filters([$category->id], true, [$footag->id, $bartag->id]);
// Slot data.
$randomslotdata = new \stdClass();
@@ -401,26 +284,26 @@ class local_structure_slot_random_test extends \advanced_testcase {
// Insert the random question to the quiz.
$randomslot = new \mod_quiz\local\structure\slot_random($randomslotdata);
- $randomslot->set_tags([$footag, $bartag]);
- $randomslot->set_filter_condition($filtercondition);
+ $randomslot->set_filter_condition(json_encode($filtercondition));
$randomslot->insert(1); // Put the question on the first page of the quiz.
$slots = qbank_helper::get_question_structure($quiz->id, $quizcontext);
$quizslot = reset($slots);
- $this->assertEquals($category->id, $quizslot->category);
- $this->assertEquals(1, $quizslot->randomrecurse);
- $this->assertEquals(1, $quizslot->maxmark);
- $tagspropery = $quizslot->randomtags;
+ $filter = $quizslot->filtercondition['filter'];
- $this->assertCount(2, $tagspropery);
+ $this->assertEquals($category->id, $filter['category']['values'][0]);
+ $this->assertTrue($filter['category']['filteroptions']['includesubcategories']);
+ $this->assertEquals(1, $quizslot->maxmark);
+
+ $this->assertCount(2, $filter['qtagids']['values']);
$this->assertEqualsCanonicalizing(
[
- ['tagid' => $footag->id, 'tagname' => $footag->name],
- ['tagid' => $bartag->id, 'tagname' => $bartag->name]
+ ['tagid' => $footag->id],
+ ['tagid' => $bartag->id]
],
- array_map(function($slottag) {
- return ['tagid' => $slottag->id, 'tagname' => $slottag->name];
- }, $tagspropery));
+ array_map(function($tagid) {
+ return ['tagid' => $tagid];
+ }, $filter['qtagids']['values']));
}
}
diff --git a/mod/quiz/tests/locallib_test.php b/mod/quiz/tests/locallib_test.php
index 83b368da141..9a79fab38d9 100644
--- a/mod/quiz/tests/locallib_test.php
+++ b/mod/quiz/tests/locallib_test.php
@@ -31,7 +31,7 @@ defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
-
+require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
/**
* Unit tests for (some of) mod/quiz/locallib.php.
@@ -41,6 +41,8 @@ require_once($CFG->dirroot . '/mod/quiz/locallib.php');
*/
class locallib_test extends \advanced_testcase {
+ use \quiz_question_helper_test_trait;
+
public function test_quiz_rescale_grade() {
$quiz = new \stdClass();
$quiz->decimalpoints = 2;
@@ -523,7 +525,7 @@ class locallib_test extends \advanced_testcase {
$tagids[] = $tagobjects[$tagname]->id;
}
}
- quiz_add_random_questions($quiz, 0, $cat->id, 1, false, $tagids);
+ $this->add_random_questions($quiz->id, 0, $cat->id, 1);
}
return [$quiz, $tagobjects];
diff --git a/mod/quiz/tests/quiz_question_bank_view_test.php b/mod/quiz/tests/quiz_question_bank_view_test.php
index bcefd39deeb..dff013c9526 100644
--- a/mod/quiz/tests/quiz_question_bank_view_test.php
+++ b/mod/quiz/tests/quiz_question_bank_view_test.php
@@ -49,7 +49,8 @@ class quiz_question_bank_view_test extends \advanced_testcase {
// Create a question in the default category.
$contexts = new question_edit_contexts($context);
- $cat = question_make_default_categories($contexts->all());
+ question_make_default_categories($contexts->all());
+ $cat = question_get_default_category($context->id);
$questiondata = $questiongenerator->create_question('numerical', null,
['name' => 'Example question', 'category' => $cat->id]);
@@ -58,17 +59,19 @@ class quiz_question_bank_view_test extends \advanced_testcase {
$cache->delete($questiondata->id);
// Generate the view.
- $view = new custom_view($contexts, new \moodle_url('/'), $course, $cm, $quiz);
- ob_start();
- $pagevars = [
+ $params = [
'qpage' => 0,
'qperpage' => 20,
'cat' => $cat->id . ',' . $context->id,
'recurse' => false,
'showhidden' => false,
- 'qbshowtext' => false
+ 'qbshowtext' => false,
+ 'tabname' => 'editq'
];
- $view->display($pagevars, 'editq');
+ $extraparams = ['cmid' => $cm->id];
+ $view = new custom_view($contexts, new \moodle_url('/'), $course, $cm, $params, $extraparams);
+ ob_start();
+ $view->display();
$html = ob_get_clean();
// Verify the output includes the expected question.
diff --git a/mod/quiz/tests/quiz_question_helper_test_trait.php b/mod/quiz/tests/quiz_question_helper_test_trait.php
index 174ab2adc31..198c41e9968 100644
--- a/mod/quiz/tests/quiz_question_helper_test_trait.php
+++ b/mod/quiz/tests/quiz_question_helper_test_trait.php
@@ -92,7 +92,7 @@ trait quiz_question_helper_test_trait {
$cat = $questiongenerator->create_question_category($override);
$questiongenerator->create_question('truefalse', null, ['category' => $cat->id]);
$questiongenerator->create_question('essay', null, ['category' => $cat->id]);
- quiz_add_random_questions($quiz, 0, $cat->id, 1, false);
+ $this->add_random_questions($quiz->id, 0, $cat->id, 1);
}
/**
@@ -180,4 +180,28 @@ trait quiz_question_helper_test_trait {
protected function duplicate_quiz($course, $quiz): ?\cm_info {
return duplicate_module($course, get_fast_modinfo($course)->get_cm($quiz->cmid));
}
+
+ /**
+ * Add random questions to a quiz, with a filter condition based on a category ID.
+ *
+ * @param int $quizid The quiz to add the questions to.
+ * @param int $page The page number to add the questions to.
+ * @param int $categoryid The category ID to use for the filter condition.
+ * @param int $number The number of questions to add.
+ * @return void
+ */
+ protected function add_random_questions(int $quizid, int $page, int $categoryid, int $number): void {
+ $settings = quiz_settings::create($quizid);
+ $structure = \mod_quiz\structure::create_for_quiz($settings);
+ $filtercondition = [
+ 'filter' => [
+ 'category' => [
+ 'jointype' => \qbank_managecategories\category_condition::JOINTYPE_DEFAULT,
+ 'values' => [$categoryid],
+ 'filteroptions' => ['includesubcategories' => false],
+ ],
+ ],
+ ];
+ $structure->add_random_questions($page, $number, $filtercondition);
+ }
}
diff --git a/mod/quiz/tests/quiz_question_restore_test.php b/mod/quiz/tests/quiz_question_restore_test.php
index afc77a713ce..f08679b4d61 100644
--- a/mod/quiz/tests/quiz_question_restore_test.php
+++ b/mod/quiz/tests/quiz_question_restore_test.php
@@ -470,7 +470,7 @@ class quiz_question_restore_test extends \advanced_testcase {
quiz_add_quiz_question($saq->id, $quiz, 1, 3);
quiz_add_quiz_question($numq->id, $quiz, 2, 2);
quiz_add_quiz_question($matchq->id, $quiz, 3, 1);
- quiz_add_random_questions($quiz, 3, $randomcat->id, 2, false);
+ $this->add_random_questions($quiz->id, 3, $randomcat->id, 2);
$quizobj = quiz_settings::create($quiz->id, $user1->id);
$originalstructure = \mod_quiz\structure::create_for_quiz($quizobj);
@@ -508,4 +508,65 @@ class quiz_question_restore_test extends \advanced_testcase {
$this->assertEquals($originalslot->maxmark, $restoredslot->maxmark);
}
}
+
+ /**
+ * Test pre 4.3 quiz restore for random question filter conditions.
+ *
+ * @covers \restore_question_set_reference_data_trait::process_question_set_reference
+ */
+ public function test_pre_43_quiz_restore_for_random_question_filtercondition() {
+ global $USER, $DB;
+ $this->resetAfterTest();
+ $backupid = 'abc';
+ $backuppath = make_backup_temp_directory($backupid);
+ get_file_packer('application/vnd.moodle.backup')->extract_to_pathname(
+ __DIR__ . "/fixtures/moodle_42_random_question.mbz", $backuppath);
+
+ // Do the restore to new course with default settings.
+ $categoryid = $DB->get_field_sql("SELECT MIN(id) FROM {course_categories}");
+ $newcourseid = \restore_dbops::create_new_course('Test fullname', 'Test shortname', $categoryid);
+ $rc = new \restore_controller($backupid, $newcourseid, \backup::INTERACTIVE_NO, \backup::MODE_GENERAL, $USER->id,
+ \backup::TARGET_NEW_COURSE);
+
+ $this->assertTrue($rc->execute_precheck());
+ $rc->execute_plan();
+ $rc->destroy();
+
+ // Get the information about the resulting course and check that it is set up correctly.
+ $modinfo = get_fast_modinfo($newcourseid);
+ $quiz = array_values($modinfo->get_instances_of('quiz'))[0];
+ $quizobj = \mod_quiz\quiz_settings::create($quiz->instance);
+ $structure = \mod_quiz\structure::create_for_quiz($quizobj);
+
+ // Count the questions in quiz qbank.
+ $context = \context_module::instance(get_coursemodule_from_instance("quiz", $quizobj->get_quizid(), $newcourseid)->id);
+ $this->assertEquals(2, $this->question_count($context->id));
+
+ // Are the correct slots returned?
+ $slots = $structure->get_slots();
+ $this->assertCount(1, $slots);
+
+ // Check that the filtercondition now matches the 4.3 structure.
+ foreach ($slots as $slot) {
+ $setreference = $DB->get_record('question_set_references',
+ ['itemid' => $slot->id, 'component' => 'mod_quiz', 'questionarea' => 'slot']);
+ $filterconditions = json_decode($setreference->filtercondition, true);
+ $this->assertArrayHasKey('cat', $filterconditions);
+ $this->assertArrayHasKey('jointype', $filterconditions);
+ $this->assertArrayHasKey('qpage', $filterconditions);
+ $this->assertArrayHasKey('qperpage', $filterconditions);
+ $this->assertArrayHasKey('filter', $filterconditions);
+ $this->assertArrayHasKey('category', $filterconditions['filter']);
+ $this->assertArrayHasKey('qtagids', $filterconditions['filter']);
+ $this->assertArrayNotHasKey('questioncategoryid', $filterconditions);
+ $this->assertArrayNotHasKey('tags', $filterconditions);
+ $expectedtags = \core_tag_tag::get_by_name_bulk(1, ['foo', 'bar']);
+ $expectedtagids = array_values(array_map(fn($expectedtag) => $expectedtag->id, $expectedtags));
+ $this->assertEquals($expectedtagids, $filterconditions['filter']['qtagids']['values']);
+ $expectedcategory = $DB->get_record('question_categories', ['idnumber' => 'RAND']);
+ $this->assertEquals($expectedcategory->id, $filterconditions['filter']['category']['values'][0]);
+ $expectedcat = implode(',', [$expectedcategory->id, $expectedcategory->contextid]);
+ $this->assertEquals($expectedcat, $filterconditions['cat']);
+ }
+ }
}
diff --git a/mod/quiz/tests/structure_test.php b/mod/quiz/tests/structure_test.php
index 759dbba4667..6e198469889 100644
--- a/mod/quiz/tests/structure_test.php
+++ b/mod/quiz/tests/structure_test.php
@@ -16,6 +16,11 @@
namespace mod_quiz;
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
+
/**
* Unit tests for quiz events.
*
@@ -26,6 +31,8 @@ namespace mod_quiz;
*/
class structure_test extends \advanced_testcase {
+ use \quiz_question_helper_test_trait;
+
/**
* Create a course with an empty quiz.
* @return array with three elements quiz, cm and course.
@@ -691,7 +698,7 @@ class structure_test extends \advanced_testcase {
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $questiongenerator->create_question_category();
- quiz_add_random_questions($quizobj->get_quiz(), 1, $cat->id, 1, false);
+ $this->add_random_questions($quizobj->get_quizid(), 1, $cat->id, 1);
$structure = structure::create_for_quiz($quizobj);
$sql = 'SELECT qsr.*
FROM {question_set_references} qsr
diff --git a/mod/quiz/version.php b/mod/quiz/version.php
index f72a5807d66..890e52d691a 100644
--- a/mod/quiz/version.php
+++ b/mod/quiz/version.php
@@ -24,6 +24,6 @@
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2023082400;
+$plugin->version = 2023083100;
$plugin->requires = 2023041800;
$plugin->component = 'mod_quiz';
diff --git a/question/amd/build/filter.min.js b/question/amd/build/filter.min.js
new file mode 100644
index 00000000000..89dbed38554
--- /dev/null
+++ b/question/amd/build/filter.min.js
@@ -0,0 +1,10 @@
+define("core_question/filter",["exports","core/datafilter","core/notification","core/datafilter/selectors","core/templates","core/fragment"],(function(_exports,_datafilter,_notification,_selectors,_templates,_fragment){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
+/**
+ * Question bank filter management.
+ *
+ * @module core_question/filter
+ * @copyright 2021 Tomo Tsuyuki
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_datafilter=_interopRequireDefault(_datafilter),_notification=_interopRequireDefault(_notification),_selectors=_interopRequireDefault(_selectors),_templates=_interopRequireDefault(_templates),_fragment=_interopRequireDefault(_fragment);_exports.init=(filterRegionId,defaultcourseid,defaultcategoryid,perpage,contextId,component,callback,view,cmid,pagevars,extraparams)=>{var _document$querySelect,_document$querySelect2;const SELECTORS_QUESTION_CONTAINER_ID="#questionscontainer",SELECTORS_QUESTION_TABLE="#questionscontainer table",SELECTORS_SORT_LINK="#questionscontainer div.sorters a",SELECTORS_PAGINATION_LINK="#questionscontainer a[href].page-link",SELECTORS_LASTCHANGED_FIELD="#questionsubmit input[name=lastchanged]",SELECTORS_BULK_ACTIONS="#bulkactionsui-container input",SELECTORS_EDIT_SWITCH=".editmode-switch-form input[name=setmode]",SELECTORS_EDIT_SWITCH_URL=".editmode-switch-form input[name=pageurl]",filterSet=document.querySelector("#".concat(filterRegionId)),filterCondition={cat:defaultcategoryid,courseid:defaultcourseid,filter:{},jointype:0,qpage:0,qperpage:perpage,sortdata:{},tabname:"questions"},defaultSort=null===(_document$querySelect=document.querySelector(SELECTORS_QUESTION_TABLE))||void 0===_document$querySelect||null===(_document$querySelect2=_document$querySelect.dataset)||void 0===_document$querySelect2?void 0:_document$querySelect2.defaultsort;defaultSort&&(filterCondition.sortData=JSON.parse(defaultSort));const coreFilter=new _datafilter.default(filterSet,((filterdata,pendingPromise)=>{var _document$querySelect3,_document$querySelect4;filterdata&&(filterCondition.jointype=parseInt(filterSet.dataset.filterverb,10),delete filterdata.jointype,filterCondition.filter=filterdata,0!==Object.keys(filterdata).length&&(isNaN(filterCondition.jointype)||(filterdata.jointype=filterCondition.jointype),updateUrlParams(filterdata)));const viewData={view:view,cmid:cmid,filtercondition:JSON.stringify(filterCondition),extraparams:extraparams,filterquery:"",lastchanged:null!==(_document$querySelect3=null===(_document$querySelect4=document.querySelector(SELECTORS_LASTCHANGED_FIELD))||void 0===_document$querySelect4?void 0:_document$querySelect4.value)&&void 0!==_document$querySelect3?_document$querySelect3:null};_fragment.default.loadFragment(component,callback,contextId,viewData).then(((questionhtml,jsfooter)=>{const questionscontainer=document.querySelector(SELECTORS_QUESTION_CONTAINER_ID);return void 0===questionhtml&&(questionhtml=""),void 0===jsfooter&&(jsfooter=""),_templates.default.replaceNodeContents(questionscontainer,questionhtml,jsfooter),pendingPromise&&pendingPromise.resolve(),{questionhtml:questionhtml,jsfooter:jsfooter}})).catch(_notification.default.exception)}));coreFilter.activeFilters={},coreFilter.init();const updateUrlParams=filters=>{const url=new URL(location.href),filterQuery=JSON.stringify(filters);url.searchParams.set("filter",filterQuery),history.pushState(filters,"",url),document.querySelectorAll(SELECTORS_BULK_ACTIONS).forEach((bulkAction=>{const actionUrl=new URL(bulkAction.formAction),returnUrl=new URL(actionUrl.searchParams.get("returnurl"));returnUrl.searchParams.set("filter",filterQuery),actionUrl.searchParams.set("returnurl",returnUrl),bulkAction.formAction=actionUrl}));const editSwitch=document.querySelector(SELECTORS_EDIT_SWITCH),editSwitchUrlInput=document.querySelector(SELECTORS_EDIT_SWITCH_URL),editSwitchUrl=new URL(editSwitchUrlInput.value);editSwitchUrl.searchParams.set("filter",filterQuery),editSwitchUrlInput.value=editSwitchUrl,editSwitch.dataset.pageurl=editSwitchUrl};let initialFilters;document.addEventListener("click",(e=>{const sortableLink=e.target.closest(SELECTORS_SORT_LINK),paginationLink=e.target.closest(SELECTORS_PAGINATION_LINK),clearLink=e.target.closest(_selectors.default.filterset.actions.resetFilters);if(sortableLink){e.preventDefault();const oldSort=filterCondition.sortdata;filterCondition.sortdata={},filterCondition.sortdata[sortableLink.dataset.sortname]=sortableLink.dataset.sortorder;for(const sortname in oldSort)sortname!==sortableLink.dataset.sortname&&(filterCondition.sortdata[sortname]=oldSort[sortname]);filterCondition.qpage=0,coreFilter.updateTableFromFilter()}if(paginationLink){e.preventDefault();const paginationURL=new URL(paginationLink.getAttribute("href")),qpage=paginationURL.searchParams.get("qpage");null!==paginationURL.search&&(filterCondition.qpage=qpage,coreFilter.updateTableFromFilter())}clearLink&&(()=>{const queryString=location.search,urlParams=new URLSearchParams(queryString);if(urlParams.has("cmid")){const cleanedUrl=new URL(location.href.replace(location.search,""));cleanedUrl.searchParams.set("cmid",urlParams.get("cmid")),history.pushState({},"",cleanedUrl)}if(urlParams.has("courseid")){const cleanedUrl=new URL(location.href.replace(location.search,""));cleanedUrl.searchParams.set("courseid",urlParams.get("courseid")),history.pushState({},"",cleanedUrl)}})()}));let jointype=null;if((pagevars=JSON.parse(pagevars)).filter&&(initialFilters=pagevars.filter,pagevars.jointype&&(jointype=pagevars.jointype)),0!==Object.entries(initialFilters).length){const emptyFilterRow=filterSet.querySelector(_selectors.default.filterset.regions.emptyFilterRow);emptyFilterRow&&emptyFilterRow.remove();let rowcount=0;for(const urlFilter in initialFilters){if("jointype"===urlFilter){jointype=initialFilters[urlFilter];continue}rowcount+=1;const filterdata={filtertype:urlFilter,values:initialFilters[urlFilter].values,jointype:initialFilters[urlFilter].jointype,filteroptions:initialFilters[urlFilter].filteroptions,rownum:rowcount};coreFilter.addFilterRow(filterdata)}coreFilter.filterSet.dataset.filterverb=jointype;const join=coreFilter.filterSet.querySelector(_selectors.default.filterset.fields.join);join.querySelectorAll('option:not([value="'.concat(jointype,'"])')).forEach((option=>option.remove())),join.disabled=!0}}}));
+
+//# sourceMappingURL=filter.min.js.map
\ No newline at end of file
diff --git a/question/amd/build/filter.min.js.map b/question/amd/build/filter.min.js.map
new file mode 100644
index 00000000000..112e8cd05b0
--- /dev/null
+++ b/question/amd/build/filter.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"filter.min.js","sources":["../src/filter.js"],"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 .\n\n/**\n * Question bank filter management.\n *\n * @module core_question/filter\n * @copyright 2021 Tomo Tsuyuki \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport CoreFilter from 'core/datafilter';\nimport Notification from 'core/notification';\nimport Selectors from 'core/datafilter/selectors';\nimport Templates from 'core/templates';\nimport Fragment from 'core/fragment';\n\n/**\n * Initialise the question bank filter on the element with the given id.\n *\n * @param {String} filterRegionId ID of the HTML element containing the filters.\n * @param {String} defaultcourseid Course ID for the default course to pass back to the view.\n * @param {String} defaultcategoryid Question bank category ID for the default course to pass back to the view.\n * @param {Number} perpage The number of questions to display per page.\n * @param {Number} contextId Context ID of the question bank view.\n * @param {string} component Frankenstyle name of the component for the fragment API callback (e.g. core_question)\n * @param {string} callback Name of the callback for the fragment API (e.g question_data)\n * @param {string} view The class name of the question bank view class used for this page.\n * @param {Number} cmid If we are in an activitiy, the course module ID.\n * @param {string} pagevars JSON-encoded parameters from passed from the view, including filters and jointype.\n * @param {string} extraparams JSON-encoded additional parameters specific to this view class, used for re-rendering the view.\n */\nexport const init = (\n filterRegionId,\n defaultcourseid,\n defaultcategoryid,\n perpage,\n contextId,\n component,\n callback,\n view,\n cmid,\n pagevars,\n extraparams\n) => {\n\n const SELECTORS = {\n QUESTION_CONTAINER_ID: '#questionscontainer',\n QUESTION_TABLE: '#questionscontainer table',\n SORT_LINK: '#questionscontainer div.sorters a',\n PAGINATION_LINK: '#questionscontainer a[href].page-link',\n LASTCHANGED_FIELD: '#questionsubmit input[name=lastchanged]',\n BULK_ACTIONS: '#bulkactionsui-container input',\n MENU_ACTIONS: '.menu-action',\n EDIT_SWITCH: '.editmode-switch-form input[name=setmode]',\n EDIT_SWITCH_URL: '.editmode-switch-form input[name=pageurl]',\n };\n\n const filterSet = document.querySelector(`#${filterRegionId}`);\n\n const filterCondition = {\n cat: defaultcategoryid,\n courseid: defaultcourseid,\n filter: {},\n jointype: 0,\n qpage: 0,\n qperpage: perpage,\n sortdata: {},\n tabname: 'questions',\n };\n\n const defaultSort = document.querySelector(SELECTORS.QUESTION_TABLE)?.dataset?.defaultsort;\n if (defaultSort) {\n filterCondition.sortData = JSON.parse(defaultSort);\n }\n\n let filterQuery = '';\n\n /**\n * Retrieve table data.\n *\n * @param {Object} filterdata data\n * @param {Promise} pendingPromise pending promise\n */\n const applyFilter = (filterdata, pendingPromise) => {\n // Reload the questions based on the specified filters. If no filters are provided,\n // use the default category filter condition.\n if (filterdata) {\n // Main join types.\n filterCondition.jointype = parseInt(filterSet.dataset.filterverb, 10);\n delete filterdata.jointype;\n // Retrieve filter info.\n filterCondition.filter = filterdata;\n if (Object.keys(filterdata).length !== 0) {\n if (!isNaN(filterCondition.jointype)) {\n filterdata.jointype = filterCondition.jointype;\n }\n updateUrlParams(filterdata);\n }\n }\n // Load questions for first page.\n const viewData = {\n view: view,\n cmid: cmid,\n filtercondition: JSON.stringify(filterCondition),\n extraparams: extraparams,\n filterquery: filterQuery,\n lastchanged: document.querySelector(SELECTORS.LASTCHANGED_FIELD)?.value ?? null\n };\n Fragment.loadFragment(component, callback, contextId, viewData)\n // Render questions for first page and pagination.\n .then((questionhtml, jsfooter) => {\n const questionscontainer = document.querySelector(SELECTORS.QUESTION_CONTAINER_ID);\n if (questionhtml === undefined) {\n questionhtml = '';\n }\n if (jsfooter === undefined) {\n jsfooter = '';\n }\n Templates.replaceNodeContents(questionscontainer, questionhtml, jsfooter);\n // Resolve filter promise.\n if (pendingPromise) {\n pendingPromise.resolve();\n }\n return {questionhtml, jsfooter};\n })\n .catch(Notification.exception);\n };\n\n // Init core filter processor with apply callback.\n const coreFilter = new CoreFilter(filterSet, applyFilter);\n coreFilter.activeFilters = {}; // Unset useless courseid filter.\n coreFilter.init();\n\n /**\n * Update URL Param based upon the current filter.\n *\n * @param {Object} filters Active filters.\n */\n const updateUrlParams = (filters) => {\n const url = new URL(location.href);\n const filterQuery = JSON.stringify(filters);\n url.searchParams.set('filter', filterQuery);\n history.pushState(filters, '', url);\n document.querySelectorAll(SELECTORS.BULK_ACTIONS).forEach(bulkAction => {\n const actionUrl = new URL(bulkAction.formAction);\n const returnUrl = new URL(actionUrl.searchParams.get('returnurl'));\n returnUrl.searchParams.set('filter', filterQuery);\n actionUrl.searchParams.set('returnurl', returnUrl);\n bulkAction.formAction = actionUrl;\n });\n const editSwitch = document.querySelector(SELECTORS.EDIT_SWITCH);\n const editSwitchUrlInput = document.querySelector(SELECTORS.EDIT_SWITCH_URL);\n const editSwitchUrl = new URL(editSwitchUrlInput.value);\n editSwitchUrl.searchParams.set('filter', filterQuery);\n editSwitchUrlInput.value = editSwitchUrl;\n editSwitch.dataset.pageurl = editSwitchUrl;\n };\n\n /**\n * Cleans URL parameters.\n */\n const cleanUrlParams = () => {\n const queryString = location.search;\n const urlParams = new URLSearchParams(queryString);\n if (urlParams.has('cmid')) {\n const cleanedUrl = new URL(location.href.replace(location.search, ''));\n cleanedUrl.searchParams.set('cmid', urlParams.get('cmid'));\n history.pushState({}, '', cleanedUrl);\n }\n\n if (urlParams.has('courseid')) {\n const cleanedUrl = new URL(location.href.replace(location.search, ''));\n cleanedUrl.searchParams.set('courseid', urlParams.get('courseid'));\n history.pushState({}, '', cleanedUrl);\n }\n };\n\n // Add listeners for the sorting, paging and clear actions.\n document.addEventListener('click', e => {\n const sortableLink = e.target.closest(SELECTORS.SORT_LINK);\n const paginationLink = e.target.closest(SELECTORS.PAGINATION_LINK);\n const clearLink = e.target.closest(Selectors.filterset.actions.resetFilters);\n if (sortableLink) {\n e.preventDefault();\n const oldSort = filterCondition.sortdata;\n filterCondition.sortdata = {};\n filterCondition.sortdata[sortableLink.dataset.sortname] = sortableLink.dataset.sortorder;\n for (const sortname in oldSort) {\n if (sortname !== sortableLink.dataset.sortname) {\n filterCondition.sortdata[sortname] = oldSort[sortname];\n }\n }\n filterCondition.qpage = 0;\n coreFilter.updateTableFromFilter();\n }\n if (paginationLink) {\n e.preventDefault();\n const paginationURL = new URL(paginationLink.getAttribute(\"href\"));\n const qpage = paginationURL.searchParams.get('qpage');\n if (paginationURL.search !== null) {\n filterCondition.qpage = qpage;\n coreFilter.updateTableFromFilter();\n }\n }\n if (clearLink) {\n cleanUrlParams();\n }\n });\n\n // Run apply filter at page load.\n pagevars = JSON.parse(pagevars);\n let initialFilters;\n let jointype = null;\n if (pagevars.filter) {\n // Load initial filter based on page vars.\n initialFilters = pagevars.filter;\n if (pagevars.jointype) {\n jointype = pagevars.jointype;\n }\n }\n\n if (Object.entries(initialFilters).length !== 0) {\n // Remove the default empty filter row.\n const emptyFilterRow = filterSet.querySelector(Selectors.filterset.regions.emptyFilterRow);\n if (emptyFilterRow) {\n emptyFilterRow.remove();\n }\n\n // Add filters.\n let rowcount = 0;\n for (const urlFilter in initialFilters) {\n if (urlFilter === 'jointype') {\n jointype = initialFilters[urlFilter];\n continue;\n }\n // Add each filter row.\n rowcount += 1;\n const filterdata = {\n filtertype: urlFilter,\n values: initialFilters[urlFilter].values,\n jointype: initialFilters[urlFilter].jointype,\n filteroptions: initialFilters[urlFilter].filteroptions,\n rownum: rowcount\n };\n coreFilter.addFilterRow(filterdata);\n }\n coreFilter.filterSet.dataset.filterverb = jointype;\n\n // Since we must filter by category, it does not make sense to allow the top-level \"match any\" or \"match none\" conditions,\n // as this would exclude the category. Remove those options and disable the select.\n const join = coreFilter.filterSet.querySelector(Selectors.filterset.fields.join);\n join.querySelectorAll(`option:not([value=\"${jointype}\"])`).forEach((option) => option.remove());\n join.disabled = true;\n }\n};\n"],"names":["filterRegionId","defaultcourseid","defaultcategoryid","perpage","contextId","component","callback","view","cmid","pagevars","extraparams","SELECTORS","filterSet","document","querySelector","filterCondition","cat","courseid","filter","jointype","qpage","qperpage","sortdata","tabname","defaultSort","_document$querySelect","dataset","_document$querySelect2","defaultsort","sortData","JSON","parse","coreFilter","CoreFilter","filterdata","pendingPromise","parseInt","filterverb","Object","keys","length","isNaN","updateUrlParams","viewData","filtercondition","stringify","filterquery","lastchanged","_document$querySelect4","value","loadFragment","then","questionhtml","jsfooter","questionscontainer","undefined","replaceNodeContents","resolve","catch","Notification","exception","activeFilters","init","filters","url","URL","location","href","filterQuery","searchParams","set","history","pushState","querySelectorAll","forEach","bulkAction","actionUrl","formAction","returnUrl","get","editSwitch","editSwitchUrlInput","editSwitchUrl","pageurl","initialFilters","addEventListener","e","sortableLink","target","closest","paginationLink","clearLink","Selectors","filterset","actions","resetFilters","preventDefault","oldSort","sortname","sortorder","updateTableFromFilter","paginationURL","getAttribute","search","queryString","urlParams","URLSearchParams","has","cleanedUrl","replace","cleanUrlParams","entries","emptyFilterRow","regions","remove","rowcount","urlFilter","filtertype","values","filteroptions","rownum","addFilterRow","join","fields","option","disabled"],"mappings":";;;;;;;4UA4CoB,CAChBA,eACAC,gBACAC,kBACAC,QACAC,UACAC,UACAC,SACAC,KACAC,KACAC,SACAC,sEAGMC,gCACqB,sBADrBA,yBAEc,4BAFdA,oBAGS,oCAHTA,0BAIe,wCAJfA,4BAKiB,0CALjBA,uBAMY,iCANZA,sBAQW,4CARXA,0BASe,4CAGfC,UAAYC,SAASC,yBAAkBd,iBAEvCe,gBAAkB,CACpBC,IAAKd,kBACLe,SAAUhB,gBACViB,OAAQ,GACRC,SAAU,EACVC,MAAO,EACPC,SAAUlB,QACVmB,SAAU,GACVC,QAAS,aAGPC,0CAAcX,SAASC,cAAcH,2FAAvBc,sBAAkDC,iDAAlDC,uBAA2DC,YAC3EJ,cACAT,gBAAgBc,SAAWC,KAAKC,MAAMP,oBAyDpCQ,WAAa,IAAIC,oBAAWrB,WA9Cd,CAACsB,WAAYC,oEAGzBD,aAEAnB,gBAAgBI,SAAWiB,SAASxB,UAAUc,QAAQW,WAAY,WAC3DH,WAAWf,SAElBJ,gBAAgBG,OAASgB,WACc,IAAnCI,OAAOC,KAAKL,YAAYM,SACnBC,MAAM1B,gBAAgBI,YACvBe,WAAWf,SAAWJ,gBAAgBI,UAE1CuB,gBAAgBR,oBAIlBS,SAAW,CACbpC,KAAMA,KACNC,KAAMA,KACNoC,gBAAiBd,KAAKe,UAAU9B,iBAChCL,YAAaA,YACboC,YA9BU,GA+BVC,0EAAalC,SAASC,cAAcH,sEAAvBqC,uBAAqDC,+DAAS,wBAEtEC,aAAa7C,UAAWC,SAAUF,UAAWuC,UAEjDQ,MAAK,CAACC,aAAcC,kBACXC,mBAAqBzC,SAASC,cAAcH,6CAC7B4C,IAAjBH,eACAA,aAAe,SAEFG,IAAbF,WACAA,SAAW,uBAELG,oBAAoBF,mBAAoBF,aAAcC,UAE5DlB,gBACAA,eAAesB,UAEZ,CAACL,aAAAA,aAAcC,SAAAA,aAEzBK,MAAMC,sBAAaC,cAK5B5B,WAAW6B,cAAgB,GAC3B7B,WAAW8B,aAOLpB,gBAAmBqB,gBACfC,IAAM,IAAIC,IAAIC,SAASC,MACvBC,YAActC,KAAKe,UAAUkB,SACnCC,IAAIK,aAAaC,IAAI,SAAUF,aAC/BG,QAAQC,UAAUT,QAAS,GAAIC,KAC/BnD,SAAS4D,iBAAiB9D,wBAAwB+D,SAAQC,mBAChDC,UAAY,IAAIX,IAAIU,WAAWE,YAC/BC,UAAY,IAAIb,IAAIW,UAAUP,aAAaU,IAAI,cACrDD,UAAUT,aAAaC,IAAI,SAAUF,aACrCQ,UAAUP,aAAaC,IAAI,YAAaQ,WACxCH,WAAWE,WAAaD,mBAEtBI,WAAanE,SAASC,cAAcH,uBACpCsE,mBAAqBpE,SAASC,cAAcH,2BAC5CuE,cAAgB,IAAIjB,IAAIgB,mBAAmBhC,OACjDiC,cAAcb,aAAaC,IAAI,SAAUF,aACzCa,mBAAmBhC,MAAQiC,cAC3BF,WAAWtD,QAAQyD,QAAUD,mBAwD7BE,eAjCJvE,SAASwE,iBAAiB,SAASC,UACzBC,aAAeD,EAAEE,OAAOC,QAAQ9E,qBAChC+E,eAAiBJ,EAAEE,OAAOC,QAAQ9E,2BAClCgF,UAAYL,EAAEE,OAAOC,QAAQG,mBAAUC,UAAUC,QAAQC,iBAC3DR,aAAc,CACdD,EAAEU,uBACIC,QAAUlF,gBAAgBO,SAChCP,gBAAgBO,SAAW,GAC3BP,gBAAgBO,SAASiE,aAAa7D,QAAQwE,UAAYX,aAAa7D,QAAQyE,cAC1E,MAAMD,YAAYD,QACfC,WAAaX,aAAa7D,QAAQwE,WAClCnF,gBAAgBO,SAAS4E,UAAYD,QAAQC,WAGrDnF,gBAAgBK,MAAQ,EACxBY,WAAWoE,2BAEXV,eAAgB,CAChBJ,EAAEU,uBACIK,cAAgB,IAAIpC,IAAIyB,eAAeY,aAAa,SACpDlF,MAAQiF,cAAchC,aAAaU,IAAI,SAChB,OAAzBsB,cAAcE,SACdxF,gBAAgBK,MAAQA,MACxBY,WAAWoE,yBAGfT,WA3Ce,YACba,YAActC,SAASqC,OACvBE,UAAY,IAAIC,gBAAgBF,gBAClCC,UAAUE,IAAI,QAAS,OACjBC,WAAa,IAAI3C,IAAIC,SAASC,KAAK0C,QAAQ3C,SAASqC,OAAQ,KAClEK,WAAWvC,aAAaC,IAAI,OAAQmC,UAAU1B,IAAI,SAClDR,QAAQC,UAAU,GAAI,GAAIoC,eAG1BH,UAAUE,IAAI,YAAa,OACrBC,WAAa,IAAI3C,IAAIC,SAASC,KAAK0C,QAAQ3C,SAASqC,OAAQ,KAClEK,WAAWvC,aAAaC,IAAI,WAAYmC,UAAU1B,IAAI,aACtDR,QAAQC,UAAU,GAAI,GAAIoC,cAgC1BE,UAOJ3F,SAAW,SAFfV,SAAWqB,KAAKC,MAAMtB,WAGTS,SAETkE,eAAiB3E,SAASS,OACtBT,SAASU,WACTA,SAAWV,SAASU,WAIkB,IAA1CmB,OAAOyE,QAAQ3B,gBAAgB5C,OAAc,OAEvCwE,eAAiBpG,UAAUE,cAAc8E,mBAAUC,UAAUoB,QAAQD,gBACvEA,gBACAA,eAAeE,aAIfC,SAAW,MACV,MAAMC,aAAahC,eAAgB,IAClB,aAAdgC,UAA0B,CAC1BjG,SAAWiE,eAAegC,oBAI9BD,UAAY,QACNjF,WAAa,CACfmF,WAAYD,UACZE,OAASlC,eAAegC,WAAWE,OACnCnG,SAAUiE,eAAegC,WAAWjG,SACpCoG,cAAenC,eAAegC,WAAWG,cACzCC,OAAQL,UAEZnF,WAAWyF,aAAavF,YAE5BF,WAAWpB,UAAUc,QAAQW,WAAalB,eAIpCuG,KAAO1F,WAAWpB,UAAUE,cAAc8E,mBAAUC,UAAU8B,OAAOD,MAC3EA,KAAKjD,8CAAuCtD,iBAAeuD,SAASkD,QAAWA,OAAOV,WACtFQ,KAAKG,UAAW"}
\ No newline at end of file
diff --git a/question/amd/src/filter.js b/question/amd/src/filter.js
new file mode 100644
index 00000000000..8ac0c55b31b
--- /dev/null
+++ b/question/amd/src/filter.js
@@ -0,0 +1,268 @@
+// 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 .
+
+/**
+ * Question bank filter management.
+ *
+ * @module core_question/filter
+ * @copyright 2021 Tomo Tsuyuki
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import CoreFilter from 'core/datafilter';
+import Notification from 'core/notification';
+import Selectors from 'core/datafilter/selectors';
+import Templates from 'core/templates';
+import Fragment from 'core/fragment';
+
+/**
+ * Initialise the question bank filter on the element with the given id.
+ *
+ * @param {String} filterRegionId ID of the HTML element containing the filters.
+ * @param {String} defaultcourseid Course ID for the default course to pass back to the view.
+ * @param {String} defaultcategoryid Question bank category ID for the default course to pass back to the view.
+ * @param {Number} perpage The number of questions to display per page.
+ * @param {Number} contextId Context ID of the question bank view.
+ * @param {string} component Frankenstyle name of the component for the fragment API callback (e.g. core_question)
+ * @param {string} callback Name of the callback for the fragment API (e.g question_data)
+ * @param {string} view The class name of the question bank view class used for this page.
+ * @param {Number} cmid If we are in an activitiy, the course module ID.
+ * @param {string} pagevars JSON-encoded parameters from passed from the view, including filters and jointype.
+ * @param {string} extraparams JSON-encoded additional parameters specific to this view class, used for re-rendering the view.
+ */
+export const init = (
+ filterRegionId,
+ defaultcourseid,
+ defaultcategoryid,
+ perpage,
+ contextId,
+ component,
+ callback,
+ view,
+ cmid,
+ pagevars,
+ extraparams
+) => {
+
+ const SELECTORS = {
+ QUESTION_CONTAINER_ID: '#questionscontainer',
+ QUESTION_TABLE: '#questionscontainer table',
+ SORT_LINK: '#questionscontainer div.sorters a',
+ PAGINATION_LINK: '#questionscontainer a[href].page-link',
+ LASTCHANGED_FIELD: '#questionsubmit input[name=lastchanged]',
+ BULK_ACTIONS: '#bulkactionsui-container input',
+ MENU_ACTIONS: '.menu-action',
+ EDIT_SWITCH: '.editmode-switch-form input[name=setmode]',
+ EDIT_SWITCH_URL: '.editmode-switch-form input[name=pageurl]',
+ };
+
+ const filterSet = document.querySelector(`#${filterRegionId}`);
+
+ const filterCondition = {
+ cat: defaultcategoryid,
+ courseid: defaultcourseid,
+ filter: {},
+ jointype: 0,
+ qpage: 0,
+ qperpage: perpage,
+ sortdata: {},
+ tabname: 'questions',
+ };
+
+ const defaultSort = document.querySelector(SELECTORS.QUESTION_TABLE)?.dataset?.defaultsort;
+ if (defaultSort) {
+ filterCondition.sortData = JSON.parse(defaultSort);
+ }
+
+ let filterQuery = '';
+
+ /**
+ * Retrieve table data.
+ *
+ * @param {Object} filterdata data
+ * @param {Promise} pendingPromise pending promise
+ */
+ const applyFilter = (filterdata, pendingPromise) => {
+ // Reload the questions based on the specified filters. If no filters are provided,
+ // use the default category filter condition.
+ if (filterdata) {
+ // Main join types.
+ filterCondition.jointype = parseInt(filterSet.dataset.filterverb, 10);
+ delete filterdata.jointype;
+ // Retrieve filter info.
+ filterCondition.filter = filterdata;
+ if (Object.keys(filterdata).length !== 0) {
+ if (!isNaN(filterCondition.jointype)) {
+ filterdata.jointype = filterCondition.jointype;
+ }
+ updateUrlParams(filterdata);
+ }
+ }
+ // Load questions for first page.
+ const viewData = {
+ view: view,
+ cmid: cmid,
+ filtercondition: JSON.stringify(filterCondition),
+ extraparams: extraparams,
+ filterquery: filterQuery,
+ lastchanged: document.querySelector(SELECTORS.LASTCHANGED_FIELD)?.value ?? null
+ };
+ Fragment.loadFragment(component, callback, contextId, viewData)
+ // Render questions for first page and pagination.
+ .then((questionhtml, jsfooter) => {
+ const questionscontainer = document.querySelector(SELECTORS.QUESTION_CONTAINER_ID);
+ if (questionhtml === undefined) {
+ questionhtml = '';
+ }
+ if (jsfooter === undefined) {
+ jsfooter = '';
+ }
+ Templates.replaceNodeContents(questionscontainer, questionhtml, jsfooter);
+ // Resolve filter promise.
+ if (pendingPromise) {
+ pendingPromise.resolve();
+ }
+ return {questionhtml, jsfooter};
+ })
+ .catch(Notification.exception);
+ };
+
+ // Init core filter processor with apply callback.
+ const coreFilter = new CoreFilter(filterSet, applyFilter);
+ coreFilter.activeFilters = {}; // Unset useless courseid filter.
+ coreFilter.init();
+
+ /**
+ * Update URL Param based upon the current filter.
+ *
+ * @param {Object} filters Active filters.
+ */
+ const updateUrlParams = (filters) => {
+ const url = new URL(location.href);
+ const filterQuery = JSON.stringify(filters);
+ url.searchParams.set('filter', filterQuery);
+ history.pushState(filters, '', url);
+ document.querySelectorAll(SELECTORS.BULK_ACTIONS).forEach(bulkAction => {
+ const actionUrl = new URL(bulkAction.formAction);
+ const returnUrl = new URL(actionUrl.searchParams.get('returnurl'));
+ returnUrl.searchParams.set('filter', filterQuery);
+ actionUrl.searchParams.set('returnurl', returnUrl);
+ bulkAction.formAction = actionUrl;
+ });
+ const editSwitch = document.querySelector(SELECTORS.EDIT_SWITCH);
+ const editSwitchUrlInput = document.querySelector(SELECTORS.EDIT_SWITCH_URL);
+ const editSwitchUrl = new URL(editSwitchUrlInput.value);
+ editSwitchUrl.searchParams.set('filter', filterQuery);
+ editSwitchUrlInput.value = editSwitchUrl;
+ editSwitch.dataset.pageurl = editSwitchUrl;
+ };
+
+ /**
+ * Cleans URL parameters.
+ */
+ const cleanUrlParams = () => {
+ const queryString = location.search;
+ const urlParams = new URLSearchParams(queryString);
+ if (urlParams.has('cmid')) {
+ const cleanedUrl = new URL(location.href.replace(location.search, ''));
+ cleanedUrl.searchParams.set('cmid', urlParams.get('cmid'));
+ history.pushState({}, '', cleanedUrl);
+ }
+
+ if (urlParams.has('courseid')) {
+ const cleanedUrl = new URL(location.href.replace(location.search, ''));
+ cleanedUrl.searchParams.set('courseid', urlParams.get('courseid'));
+ history.pushState({}, '', cleanedUrl);
+ }
+ };
+
+ // Add listeners for the sorting, paging and clear actions.
+ document.addEventListener('click', e => {
+ const sortableLink = e.target.closest(SELECTORS.SORT_LINK);
+ const paginationLink = e.target.closest(SELECTORS.PAGINATION_LINK);
+ const clearLink = e.target.closest(Selectors.filterset.actions.resetFilters);
+ if (sortableLink) {
+ e.preventDefault();
+ const oldSort = filterCondition.sortdata;
+ filterCondition.sortdata = {};
+ filterCondition.sortdata[sortableLink.dataset.sortname] = sortableLink.dataset.sortorder;
+ for (const sortname in oldSort) {
+ if (sortname !== sortableLink.dataset.sortname) {
+ filterCondition.sortdata[sortname] = oldSort[sortname];
+ }
+ }
+ filterCondition.qpage = 0;
+ coreFilter.updateTableFromFilter();
+ }
+ if (paginationLink) {
+ e.preventDefault();
+ const paginationURL = new URL(paginationLink.getAttribute("href"));
+ const qpage = paginationURL.searchParams.get('qpage');
+ if (paginationURL.search !== null) {
+ filterCondition.qpage = qpage;
+ coreFilter.updateTableFromFilter();
+ }
+ }
+ if (clearLink) {
+ cleanUrlParams();
+ }
+ });
+
+ // Run apply filter at page load.
+ pagevars = JSON.parse(pagevars);
+ let initialFilters;
+ let jointype = null;
+ if (pagevars.filter) {
+ // Load initial filter based on page vars.
+ initialFilters = pagevars.filter;
+ if (pagevars.jointype) {
+ jointype = pagevars.jointype;
+ }
+ }
+
+ if (Object.entries(initialFilters).length !== 0) {
+ // Remove the default empty filter row.
+ const emptyFilterRow = filterSet.querySelector(Selectors.filterset.regions.emptyFilterRow);
+ if (emptyFilterRow) {
+ emptyFilterRow.remove();
+ }
+
+ // Add filters.
+ let rowcount = 0;
+ for (const urlFilter in initialFilters) {
+ if (urlFilter === 'jointype') {
+ jointype = initialFilters[urlFilter];
+ continue;
+ }
+ // Add each filter row.
+ rowcount += 1;
+ const filterdata = {
+ filtertype: urlFilter,
+ values: initialFilters[urlFilter].values,
+ jointype: initialFilters[urlFilter].jointype,
+ filteroptions: initialFilters[urlFilter].filteroptions,
+ rownum: rowcount
+ };
+ coreFilter.addFilterRow(filterdata);
+ }
+ coreFilter.filterSet.dataset.filterverb = jointype;
+
+ // Since we must filter by category, it does not make sense to allow the top-level "match any" or "match none" conditions,
+ // as this would exclude the category. Remove those options and disable the select.
+ const join = coreFilter.filterSet.querySelector(Selectors.filterset.fields.join);
+ join.querySelectorAll(`option:not([value="${jointype}"])`).forEach((option) => option.remove());
+ join.disabled = true;
+ }
+};
diff --git a/question/bank/bulkmove/move.php b/question/bank/bulkmove/move.php
index d9e79c5c848..9f3cb9be0c9 100644
--- a/question/bank/bulkmove/move.php
+++ b/question/bank/bulkmove/move.php
@@ -75,7 +75,11 @@ if ($movequestionselected && $confirm && confirm_sesskey()) {
if ($confirm == md5($movequestionselected)) {
\qbank_bulkmove\helper::bulk_move_questions($movequestionselected, $tocategory);
}
- redirect(new moodle_url($returnurl, ['category' => "{$tocategoryid},{$contextid}"]));
+ $returnfilters = \core_question\local\bank\filter_condition_manager::update_filter_param_to_category(
+ $returnurl->param('filter'),
+ $tocategoryid,
+ );
+ redirect(new moodle_url($returnurl, ['filter' => $returnfilters]));
}
echo $OUTPUT->header();
diff --git a/question/bank/bulkmove/tests/helper_test.php b/question/bank/bulkmove/tests/helper_test.php
index a94c46fa85d..7b6fa71c634 100644
--- a/question/bank/bulkmove/tests/helper_test.php
+++ b/question/bank/bulkmove/tests/helper_test.php
@@ -120,66 +120,74 @@ class helper_test extends \advanced_testcase {
];
}
+ /**
+ * Count how many questions in the list belong to the given category.
+ *
+ * @param string $categoryid a category id
+ * @param array $questionids list of question ids
+ * @return int
+ */
+ private function count_category_questions(string $categoryid, array $questionids): int {
+ global $DB;
+ $this->assertNotEmpty($questionids);
+ list($insql, $inparams) = $DB->get_in_or_equal($questionids, SQL_PARAMS_NAMED);
+ $sql = "SELECT COUNT(q.id)
+ FROM {question} q
+ JOIN {question_versions} qv ON qv.questionid = q.id
+ JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
+ JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
+ WHERE qc.id = :categoryid
+ AND q.id $insql";
+
+ return $DB->count_records_sql($sql, array_merge(['categoryid' => $categoryid], $inparams));
+ }
+
+ /**
+ * Assert that the given category contains following questions
+ *
+ * @param string $categoryid a category id
+ * @param array $questionids list of question ids
+ * @return void
+ */
+ protected function assert_category_contains_questions(string $categoryid, array $questionids) {
+ // The category need to contain all the questions.
+ $this->assertEquals(count($questionids), $this->count_category_questions($categoryid, $questionids));
+ }
+
+ /**
+ * Assert that the given category does not contain following questions
+ *
+ * @param string $categoryid a category id
+ * @param array $questionids list of question ids
+ * @return void
+ */
+ protected function assert_category_does_not_contain_questions(string $categoryid, array $questionids) {
+ // The category does not contain any question.
+ $this->assertEquals(0, $this->count_category_questions($categoryid, $questionids));
+ }
+
/**
* Test bulk move of questions.
*
* @covers ::bulk_move_questions
*/
public function test_bulk_move_questions() {
+ global $DB;
$this->helper_setup();
- // Verify that the questions are available in the current view.
- $view = new \core_question\local\bank\view($this->contexts, new \moodle_url('/'), $this->course);
- ob_start();
- $pagevars = [
- 'qpage' => 0,
- 'qperpage' => DEFAULT_QUESTIONS_PER_PAGE,
- 'cat' => $this->cat->id . ',' . $this->context->id,
- 'recurse' => false,
- 'showhidden' => false,
- 'qbshowtext' => false
- ];
- $view->display($pagevars, 'editq');
- $html = ob_get_clean();
- $this->assertStringContainsString('Example question', $html);
- $this->assertStringContainsString('Example question second', $html);
// Get the processed question ids.
$questionlist = $this->process_question_ids_test();
+ $questionids = array_map('intval', explode(',', $questionlist));
+ // Verify that the questions are available in the current view.
+ $this->assert_category_contains_questions($this->cat->id, $questionids);
helper::bulk_move_questions($questionlist, $this->secondcategory);
// Verify the questions are not in the current category.
- $view = new \core_question\local\bank\view($this->contexts, new \moodle_url('/'), $this->course);
- ob_start();
- $pagevars = [
- 'qpage' => 0,
- 'qperpage' => DEFAULT_QUESTIONS_PER_PAGE,
- 'cat' => $this->cat->id . ',' . $this->context->id,
- 'recurse' => false,
- 'showhidden' => false,
- 'qbshowtext' => false
- ];
- $view->display($pagevars, 'editq');
- $html = ob_get_clean();
- $this->assertStringNotContainsString('Example question', $html);
- $this->assertStringNotContainsString('Example question second', $html);
+ $this->assert_category_does_not_contain_questions($this->cat->id, $questionids);
// Verify the questions are in the new category.
- $view = new \core_question\local\bank\view($this->contexts, new \moodle_url('/'), $this->course);
- ob_start();
- $pagevars = [
- 'qpage' => 0,
- 'qperpage' => DEFAULT_QUESTIONS_PER_PAGE,
- 'cat' => $this->secondcategory->id . ',' . $this->context->id,
- 'category' => $this->secondcategory->id . ',' . $this->context->id,
- 'recurse' => false,
- 'showhidden' => false,
- 'qbshowtext' => false
- ];
- $view->display($pagevars, 'editq');
- $html = ob_get_clean();
- $this->assertStringContainsString('Example question', $html);
- $this->assertStringContainsString('Example question second', $html);
+ $this->assert_category_contains_questions($this->secondcategory->id, $questionids);
}
/**
diff --git a/question/bank/columnsortorder/tests/behat/columnsortorder.feature b/question/bank/columnsortorder/tests/behat/columnsortorder.feature
index 39649fd1e3c..cba9965b00d 100644
--- a/question/bank/columnsortorder/tests/behat/columnsortorder.feature
+++ b/question/bank/columnsortorder/tests/behat/columnsortorder.feature
@@ -27,7 +27,7 @@ Feature: An plugin column can be reordered and displayed in the question bank vi
Scenario: Teacher can see proper view
Given I am on the "Test quiz Q001" "mod_quiz > question bank" page logged in as "teacher1"
- And I set the field "category" to "Question category 1"
+ When I apply question bank filter "Category" with value "Question category 1"
And I should see "Test question to be seen"
Then I should see "Teacher 1"
@@ -36,7 +36,7 @@ Feature: An plugin column can be reordered and displayed in the question bank vi
When I navigate to "Plugins > Question bank plugins > Column sort order" in site administration
And I drag "Created by (creator_name_column)" "text" and I drop it in "T (question_type_column)" "text"
And I am on the "Test quiz Q001" "mod_quiz > question bank" page logged in as "teacher1"
- And I set the field "category" to "Question category 1"
+ And I apply question bank filter "Category" with value "Question category 1"
Then ".creatorname" "css_element" should appear before ".qtype" "css_element"
Scenario: Disabling and enabling column display is proper
diff --git a/question/bank/comment/amd/build/comment.min.js b/question/bank/comment/amd/build/comment.min.js
index 47a8d787fc2..95326aadd70 100644
--- a/question/bank/comment/amd/build/comment.min.js
+++ b/question/bank/comment/amd/build/comment.min.js
@@ -6,6 +6,6 @@ define("qbank_comment/comment",["exports","core/fragment","core/str","core/modal
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Safat Shahin
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_fragment=_interopRequireDefault(_fragment),Str=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Str),_modal_events=_interopRequireDefault(_modal_events),_modal_factory=_interopRequireDefault(_modal_factory),_notification=_interopRequireDefault(_notification);_exports.init=questionSelector=>{const target=document.querySelector(questionSelector);target.addEventListener("click",(()=>{((questionId,courseID,contextId)=>{let args={questionid:questionId,courseid:courseID};_modal_factory.default.create({type:_modal_factory.default.types.SAVE_CANCEL,title:Str.get_string("commentheader","qbank_comment"),body:_fragment.default.loadFragment("qbank_comment","question_comment",contextId,args),large:!0}).then((modal=>{let root=modal.getRoot();return root.on(_modal_events.default.bodyRendered,(function(){document.querySelectorAll("div.comment-area a")[0].style.display="none"})),root.on("change","#question_comment_version_dropdown",(function(e){args.questionid=e.target.value,modal.setBody(_fragment.default.loadFragment("qbank_comment","question_comment",contextId,args))})),Str.get_strings([{key:"addcomment",component:"qbank_comment"},{key:"close",component:"qbank_comment"}]).then((strings=>{modal.setButtonText("save",strings[0]),modal.setButtonText("cancel",strings[1])})).fail(_notification.default.exception),root.on(_modal_events.default.cancel,(function(){location.reload(),modal.hide()})),root.on(_modal_events.default.save,(function(e){e.preventDefault();const submitlink=document.querySelectorAll("div.comment-area a")[0],textarea=document.querySelectorAll("div.comment-area textarea")[0];textarea.value!=textarea.getAttribute("aria-label")&&""!=textarea.value&&submitlink.click()})),root.on("click",'button[data-action="hide"]',(()=>{location.reload(),modal.hide()})),modal.show(),modal})).fail(_notification.default.exception)})(target.dataset.questionid,target.dataset.courseid,target.dataset.contextid)}))}}));
+ */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_fragment=_interopRequireDefault(_fragment),Str=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Str),_modal_events=_interopRequireDefault(_modal_events),_modal_factory=_interopRequireDefault(_modal_factory),_notification=_interopRequireDefault(_notification);_exports.init=()=>{const target=document.querySelector("#categoryquestions");null!==target&&target.addEventListener("click",(e=>{e.target.dataset.target&&e.target.dataset.target.includes("questioncommentpreview")&&((questionId,courseID,contextId)=>{let args={questionid:questionId,courseid:courseID};_modal_factory.default.create({type:_modal_factory.default.types.SAVE_CANCEL,title:Str.get_string("commentheader","qbank_comment"),body:_fragment.default.loadFragment("qbank_comment","question_comment",contextId,args),large:!0}).then((modal=>{let root=modal.getRoot();return root.on(_modal_events.default.bodyRendered,(function(){document.querySelectorAll("div.comment-area a")[0].style.display="none"})),root.on("change","#question_comment_version_dropdown",(function(e){args.questionid=e.target.value,modal.setBody(_fragment.default.loadFragment("qbank_comment","question_comment",contextId,args))})),Str.get_strings([{key:"addcomment",component:"qbank_comment"},{key:"close",component:"qbank_comment"}]).then((strings=>{modal.setButtonText("save",strings[0]),modal.setButtonText("cancel",strings[1])})).fail(_notification.default.exception),root.on(_modal_events.default.cancel,(function(){location.reload(),modal.hide()})),root.on(_modal_events.default.save,(function(e){e.preventDefault();const submitlink=document.querySelectorAll("div.comment-area a")[0],textarea=document.querySelectorAll("div.comment-area textarea")[0];textarea.value!=textarea.getAttribute("aria-label")&&""!=textarea.value&&submitlink.click()})),root.on("click",'button[data-action="hide"]',(()=>{location.reload(),modal.hide()})),modal.show(),modal})).fail(_notification.default.exception)})(e.target.dataset.questionid,e.target.dataset.courseid,e.target.dataset.contextid)}))}}));
//# sourceMappingURL=comment.min.js.map
\ No newline at end of file
diff --git a/question/bank/comment/amd/build/comment.min.js.map b/question/bank/comment/amd/build/comment.min.js.map
index 2c2442c499c..bf0715609e8 100644
--- a/question/bank/comment/amd/build/comment.min.js.map
+++ b/question/bank/comment/amd/build/comment.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"comment.min.js","sources":["../src/comment.js"],"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 .\n\n/**\n * Column selector js.\n *\n * @module qbank_comment/comment\n * @copyright 2021 Catalyst IT Australia Pty Ltd\n * @author Safat Shahin \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Fragment from 'core/fragment';\nimport * as Str from 'core/str';\nimport ModalEvents from 'core/modal_events';\nimport ModalFactory from 'core/modal_factory';\nimport Notification from 'core/notification';\n\n/**\n * Event listeners for the module.\n *\n * @method clickEvent\n * @param {Number} questionId\n * @param {Number} courseID\n * @param {Number} contextId\n */\nconst commentEvent = (questionId, courseID, contextId) => {\n let args = {\n questionid: questionId,\n courseid: courseID\n };\n ModalFactory.create({\n type: ModalFactory.types.SAVE_CANCEL,\n title: Str.get_string('commentheader', 'qbank_comment'),\n body: Fragment.loadFragment('qbank_comment', 'question_comment', contextId, args),\n large: true,\n }).then((modal) => {\n let root = modal.getRoot();\n\n // Don't display the default add comment link in the modal.\n root.on(ModalEvents.bodyRendered, function() {\n const submitlink = document.querySelectorAll(\"div.comment-area a\")[0];\n submitlink.style.display = 'none';\n });\n\n // Version selection event.\n root.on('change', '#question_comment_version_dropdown', function(e) {\n args.questionid = e.target.value;\n modal.setBody(Fragment.loadFragment('qbank_comment', 'question_comment', contextId, args));\n });\n\n // Get the required strings and updated the modal button text labels.\n Str.get_strings([\n {key: 'addcomment', component: 'qbank_comment'},\n {key: 'close', component: 'qbank_comment'},\n ]).then((strings) => {\n modal.setButtonText('save', strings[0]);\n modal.setButtonText('cancel', strings[1]);\n return;\n }).fail(Notification.exception);\n\n root.on(ModalEvents.cancel, function() {\n location.reload();\n modal.hide();\n });\n\n // Handle adding the comment when the button in the modal is clicked.\n root.on(ModalEvents.save, function(e) {\n e.preventDefault();\n const submitlink = document.querySelectorAll(\"div.comment-area a\")[0];\n const textarea = document.querySelectorAll(\"div.comment-area textarea\")[0];\n\n // Check there is a valid comment to add, and trigger adding if there is.\n if (textarea.value != textarea.getAttribute('aria-label') && textarea.value != '') {\n submitlink.click();\n }\n\n });\n root.on('click', 'button[data-action=\"hide\"]', () => {\n location.reload();\n modal.hide();\n });\n modal.show();\n return modal;\n }).fail(Notification.exception);\n};\n\n/**\n * Entrypoint of the js.\n *\n * @method init\n * @param {string} questionSelector the question comment identifier.\n */\nexport const init = (questionSelector) => {\n const target = document.querySelector(questionSelector);\n target.addEventListener('click', () => {\n // Call for the event listener to listed for clicks in any comment count row.\n commentEvent(target.dataset.questionid, target.dataset.courseid, target.dataset.contextid);\n });\n};\n"],"names":["questionSelector","target","document","querySelector","addEventListener","questionId","courseID","contextId","args","questionid","courseid","create","type","ModalFactory","types","SAVE_CANCEL","title","Str","get_string","body","Fragment","loadFragment","large","then","modal","root","getRoot","on","ModalEvents","bodyRendered","querySelectorAll","style","display","e","value","setBody","get_strings","key","component","strings","setButtonText","fail","Notification","exception","cancel","location","reload","hide","save","preventDefault","submitlink","textarea","getAttribute","click","show","commentEvent","dataset","contextid"],"mappings":";;;;;;;;g8BAyGqBA,yBACXC,OAASC,SAASC,cAAcH,kBACtCC,OAAOG,iBAAiB,SAAS,KArEhB,EAACC,WAAYC,SAAUC,iBACpCC,KAAO,CACPC,WAAYJ,WACZK,SAAUJ,iCAEDK,OAAO,CAChBC,KAAMC,uBAAaC,MAAMC,YACzBC,MAAOC,IAAIC,WAAW,gBAAiB,iBACvCC,KAAMC,kBAASC,aAAa,gBAAiB,mBAAoBd,UAAWC,MAC5Ec,OAAO,IACRC,MAAMC,YACDC,KAAOD,MAAME,iBAGjBD,KAAKE,GAAGC,sBAAYC,cAAc,WACX3B,SAAS4B,iBAAiB,sBAAsB,GACxDC,MAAMC,QAAU,UAI/BP,KAAKE,GAAG,SAAU,sCAAsC,SAASM,GAC7DzB,KAAKC,WAAawB,EAAEhC,OAAOiC,MAC3BV,MAAMW,QAAQf,kBAASC,aAAa,gBAAiB,mBAAoBd,UAAWC,UAIxFS,IAAImB,YAAY,CACZ,CAACC,IAAK,aAAcC,UAAW,iBAC/B,CAACD,IAAK,QAASC,UAAW,mBAC3Bf,MAAMgB,UACLf,MAAMgB,cAAc,OAAQD,QAAQ,IACpCf,MAAMgB,cAAc,SAAUD,QAAQ,OAEvCE,KAAKC,sBAAaC,WAErBlB,KAAKE,GAAGC,sBAAYgB,QAAQ,WACxBC,SAASC,SACTtB,MAAMuB,UAIVtB,KAAKE,GAAGC,sBAAYoB,MAAM,SAASf,GAC/BA,EAAEgB,uBACIC,WAAahD,SAAS4B,iBAAiB,sBAAsB,GAC7DqB,SAAWjD,SAAS4B,iBAAiB,6BAA6B,GAGpEqB,SAASjB,OAASiB,SAASC,aAAa,eAAmC,IAAlBD,SAASjB,OAClEgB,WAAWG,WAInB5B,KAAKE,GAAG,QAAS,8BAA8B,KAC3CkB,SAASC,SACTtB,MAAMuB,UAEVvB,MAAM8B,OACC9B,SACRiB,KAAKC,sBAAaC,YAajBY,CAAatD,OAAOuD,QAAQ/C,WAAYR,OAAOuD,QAAQ9C,SAAUT,OAAOuD,QAAQC"}
\ No newline at end of file
+{"version":3,"file":"comment.min.js","sources":["../src/comment.js"],"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 .\n\n/**\n * Column selector js.\n *\n * @module qbank_comment/comment\n * @copyright 2021 Catalyst IT Australia Pty Ltd\n * @author Safat Shahin \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Fragment from 'core/fragment';\nimport * as Str from 'core/str';\nimport ModalEvents from 'core/modal_events';\nimport ModalFactory from 'core/modal_factory';\nimport Notification from 'core/notification';\n\n/**\n * Event listeners for the module.\n *\n * @method clickEvent\n * @param {Number} questionId\n * @param {Number} courseID\n * @param {Number} contextId\n */\nconst commentEvent = (questionId, courseID, contextId) => {\n let args = {\n questionid: questionId,\n courseid: courseID\n };\n ModalFactory.create({\n type: ModalFactory.types.SAVE_CANCEL,\n title: Str.get_string('commentheader', 'qbank_comment'),\n body: Fragment.loadFragment('qbank_comment', 'question_comment', contextId, args),\n large: true,\n }).then((modal) => {\n let root = modal.getRoot();\n\n // Don't display the default add comment link in the modal.\n root.on(ModalEvents.bodyRendered, function() {\n const submitlink = document.querySelectorAll(\"div.comment-area a\")[0];\n submitlink.style.display = 'none';\n });\n\n // Version selection event.\n root.on('change', '#question_comment_version_dropdown', function(e) {\n args.questionid = e.target.value;\n modal.setBody(Fragment.loadFragment('qbank_comment', 'question_comment', contextId, args));\n });\n\n // Get the required strings and updated the modal button text labels.\n Str.get_strings([\n {key: 'addcomment', component: 'qbank_comment'},\n {key: 'close', component: 'qbank_comment'},\n ]).then((strings) => {\n modal.setButtonText('save', strings[0]);\n modal.setButtonText('cancel', strings[1]);\n return;\n }).fail(Notification.exception);\n\n root.on(ModalEvents.cancel, function() {\n location.reload();\n modal.hide();\n });\n\n // Handle adding the comment when the button in the modal is clicked.\n root.on(ModalEvents.save, function(e) {\n e.preventDefault();\n const submitlink = document.querySelectorAll(\"div.comment-area a\")[0];\n const textarea = document.querySelectorAll(\"div.comment-area textarea\")[0];\n\n // Check there is a valid comment to add, and trigger adding if there is.\n if (textarea.value != textarea.getAttribute('aria-label') && textarea.value != '') {\n submitlink.click();\n }\n\n });\n root.on('click', 'button[data-action=\"hide\"]', () => {\n location.reload();\n modal.hide();\n });\n modal.show();\n return modal;\n }).fail(Notification.exception);\n};\n\n/**\n * Entrypoint of the js.\n *\n * @method init\n */\nexport const init = () => {\n const target = document.querySelector('#categoryquestions');\n if (target !== null) {\n target.addEventListener('click', (e) => {\n if (e.target.dataset.target && e.target.dataset.target.includes('questioncommentpreview')) {\n // Call for the event listener to listed for clicks in any comment count row.\n commentEvent(e.target.dataset.questionid, e.target.dataset.courseid, e.target.dataset.contextid);\n }\n });\n }\n};\n"],"names":["target","document","querySelector","addEventListener","e","dataset","includes","questionId","courseID","contextId","args","questionid","courseid","create","type","ModalFactory","types","SAVE_CANCEL","title","Str","get_string","body","Fragment","loadFragment","large","then","modal","root","getRoot","on","ModalEvents","bodyRendered","querySelectorAll","style","display","value","setBody","get_strings","key","component","strings","setButtonText","fail","Notification","exception","cancel","location","reload","hide","save","preventDefault","submitlink","textarea","getAttribute","click","show","commentEvent","contextid"],"mappings":";;;;;;;;g8BAwGoB,WACVA,OAASC,SAASC,cAAc,sBACvB,OAAXF,QACAA,OAAOG,iBAAiB,SAAUC,IAC1BA,EAAEJ,OAAOK,QAAQL,QAAUI,EAAEJ,OAAOK,QAAQL,OAAOM,SAAS,2BAtEvD,EAACC,WAAYC,SAAUC,iBACpCC,KAAO,CACPC,WAAYJ,WACZK,SAAUJ,iCAEDK,OAAO,CAChBC,KAAMC,uBAAaC,MAAMC,YACzBC,MAAOC,IAAIC,WAAW,gBAAiB,iBACvCC,KAAMC,kBAASC,aAAa,gBAAiB,mBAAoBd,UAAWC,MAC5Ec,OAAO,IACRC,MAAMC,YACDC,KAAOD,MAAME,iBAGjBD,KAAKE,GAAGC,sBAAYC,cAAc,WACX9B,SAAS+B,iBAAiB,sBAAsB,GACxDC,MAAMC,QAAU,UAI/BP,KAAKE,GAAG,SAAU,sCAAsC,SAASzB,GAC7DM,KAAKC,WAAaP,EAAEJ,OAAOmC,MAC3BT,MAAMU,QAAQd,kBAASC,aAAa,gBAAiB,mBAAoBd,UAAWC,UAIxFS,IAAIkB,YAAY,CACZ,CAACC,IAAK,aAAcC,UAAW,iBAC/B,CAACD,IAAK,QAASC,UAAW,mBAC3Bd,MAAMe,UACLd,MAAMe,cAAc,OAAQD,QAAQ,IACpCd,MAAMe,cAAc,SAAUD,QAAQ,OAEvCE,KAAKC,sBAAaC,WAErBjB,KAAKE,GAAGC,sBAAYe,QAAQ,WACxBC,SAASC,SACTrB,MAAMsB,UAIVrB,KAAKE,GAAGC,sBAAYmB,MAAM,SAAS7C,GAC/BA,EAAE8C,uBACIC,WAAalD,SAAS+B,iBAAiB,sBAAsB,GAC7DoB,SAAWnD,SAAS+B,iBAAiB,6BAA6B,GAGpEoB,SAASjB,OAASiB,SAASC,aAAa,eAAmC,IAAlBD,SAASjB,OAClEgB,WAAWG,WAInB3B,KAAKE,GAAG,QAAS,8BAA8B,KAC3CiB,SAASC,SACTrB,MAAMsB,UAEVtB,MAAM6B,OACC7B,SACRgB,KAAKC,sBAAaC,YAcTY,CAAapD,EAAEJ,OAAOK,QAAQM,WAAYP,EAAEJ,OAAOK,QAAQO,SAAUR,EAAEJ,OAAOK,QAAQoD"}
\ No newline at end of file
diff --git a/question/bank/comment/amd/src/comment.js b/question/bank/comment/amd/src/comment.js
index afab5d5d15b..b2a6e0f7e80 100644
--- a/question/bank/comment/amd/src/comment.js
+++ b/question/bank/comment/amd/src/comment.js
@@ -101,12 +101,15 @@ const commentEvent = (questionId, courseID, contextId) => {
* Entrypoint of the js.
*
* @method init
- * @param {string} questionSelector the question comment identifier.
*/
-export const init = (questionSelector) => {
- const target = document.querySelector(questionSelector);
- target.addEventListener('click', () => {
- // Call for the event listener to listed for clicks in any comment count row.
- commentEvent(target.dataset.questionid, target.dataset.courseid, target.dataset.contextid);
- });
+export const init = () => {
+ const target = document.querySelector('#categoryquestions');
+ if (target !== null) {
+ target.addEventListener('click', (e) => {
+ if (e.target.dataset.target && e.target.dataset.target.includes('questioncommentpreview')) {
+ // Call for the event listener to listed for clicks in any comment count row.
+ commentEvent(e.target.dataset.questionid, e.target.dataset.courseid, e.target.dataset.contextid);
+ }
+ });
+ }
};
diff --git a/question/bank/comment/classes/comment_count_column.php b/question/bank/comment/classes/comment_count_column.php
index 171dff3fa5a..3995cf3367b 100644
--- a/question/bank/comment/classes/comment_count_column.php
+++ b/question/bank/comment/classes/comment_count_column.php
@@ -29,6 +29,35 @@ use question_bank;
*/
class comment_count_column extends column_base {
+ /**
+ * @var bool Comments enabled or not from config.
+ */
+ protected $commentsenabled = true;
+
+ /**
+ * Load javascript module if enabled.
+ *
+ * @return void
+ */
+ public function init(): void {
+ parent::init();
+ $this->check_comments_status();
+ if ($this->commentsenabled) {
+ global $PAGE;
+ $PAGE->requires->js_call_amd('qbank_comment/comment', 'init');
+ }
+ }
+
+ /**
+ * Check if comments is turned on in the system or not.
+ */
+ protected function check_comments_status(): void {
+ global $CFG;
+ if (!$CFG->usecomments) {
+ $this->commentsenabled = false;
+ }
+ }
+
/**
* Get the name of the column, used internally.
*
@@ -54,7 +83,7 @@ class comment_count_column extends column_base {
* @param string $rowclasses Classes that can be added.
*/
protected function display_content($question, $rowclasses): void {
- global $DB, $PAGE;
+ global $DB;
$syscontext = \context_system::instance();
$args = [
'component' => 'qbank_comment',
@@ -66,8 +95,6 @@ class comment_count_column extends column_base {
$attributes = [];
if (question_has_capability_on($question, 'comment')) {
$target = 'questioncommentpreview_' . $question->id;
- $datatarget = '[data-target="' . $target . '"]';
- $PAGE->requires->js_call_amd('qbank_comment/comment', 'init', [$datatarget]);
$attributes = [
'href' => '#',
'data-target' => $target,
diff --git a/question/bank/comment/tests/behat/question_comment.feature b/question/bank/comment/tests/behat/question_comment.feature
index 5bb522fe6c6..702e88df9e1 100644
--- a/question/bank/comment/tests/behat/question_comment.feature
+++ b/question/bank/comment/tests/behat/question_comment.feature
@@ -26,7 +26,7 @@ Feature: A Teacher can comment in a question
@javascript
Scenario: Add a comment in question
Given I am on the "Test quiz" "mod_quiz > question bank" page logged in as "teacher1"
- And I set the field "Select a category" to "Test questions"
+ And I apply question bank filter "Category" with value "Test questions"
And I should see "0" on the comments column
When I click "0" on the row on the comments column
And I add "Super test comment 01" comment to question
@@ -38,7 +38,7 @@ Feature: A Teacher can comment in a question
@javascript
Scenario: Delete a comment from question
Given I am on the "Test quiz" "mod_quiz > question bank" page logged in as "teacher1"
- And I set the field "Select a category" to "Test questions"
+ And I apply question bank filter "Category" with value "Test questions"
And I should see "0" on the comments column
When I click "0" on the row on the comments column
And I add "Super test comment 01 to be deleted" comment to question
@@ -55,7 +55,7 @@ Feature: A Teacher can comment in a question
@javascript
Scenario: Preview question with comments
Given I am on the "Test quiz" "mod_quiz > question bank" page logged in as "teacher1"
- And I set the field "Select a category" to "Test questions"
+ And I apply question bank filter "Category" with value "Test questions"
And I choose "Preview" action for "First question" in the question bank
And I click on "Comments" "link"
Then I should see "Save comment"
@@ -79,7 +79,7 @@ Feature: A Teacher can comment in a question
| moodle/question:commentmine | allow |
| moodle/question:commentall | prevent |
And I am on the "Test quiz" "mod_quiz > question bank" page logged in as "teacher1"
- And I set the field "Select a category" to "Test questions"
+ And I apply question bank filter "Category" with value "Test questions"
And I choose "Preview" action for "First question" in the question bank
Then I should not see "Save comment"
And I click on "Close preview" "button"
@@ -96,6 +96,7 @@ Feature: A Teacher can comment in a question
Then I should see "Save comment"
And I log out
And I am on the "Test quiz" "mod_quiz > question bank" page logged in as "teacher2"
+ And I apply question bank filter "Category" with value "Test questions"
And I choose "Preview" action for "First question" in the question bank
Then I should not see "Save comment"
And I click on "Close preview" "button"
@@ -135,7 +136,6 @@ Feature: A Teacher can comment in a question
Given I log in as "teacher1"
And I am on the "Test quiz" "quiz activity" page
When I navigate to "Question bank" in current page administration
- And I set the field "Select a category" to "Test questions"
And I should see "First question"
And I choose "Edit question" action for "First question" in the question bank
And I set the field "id_name" to "Renamed question v2"
diff --git a/question/bank/deletequestion/amd/build/datafilter/filtertypes/hidden.min.js b/question/bank/deletequestion/amd/build/datafilter/filtertypes/hidden.min.js
new file mode 100644
index 00000000000..13cf72bf72b
--- /dev/null
+++ b/question/bank/deletequestion/amd/build/datafilter/filtertypes/hidden.min.js
@@ -0,0 +1,11 @@
+define("qbank_deletequestion/datafilter/filtertypes/hidden",["exports","core/datafilter/filtertypes/binary"],(function(_exports,_binary){var obj;
+/**
+ * Filter managing hidden questions.
+ *
+ * @module qbank_deletequestion/datafilter/filtertypes/hidden
+ * @author 2022 Ghaly Marc-Alexandre
+ * @copyright 2022 Catalyst IT Australia Pty Ltd
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_binary=(obj=_binary)&&obj.__esModule?obj:{default:obj};class _default extends _binary.default{}return _exports.default=_default,_exports.default}));
+
+//# sourceMappingURL=hidden.min.js.map
\ No newline at end of file
diff --git a/question/bank/deletequestion/amd/build/datafilter/filtertypes/hidden.min.js.map b/question/bank/deletequestion/amd/build/datafilter/filtertypes/hidden.min.js.map
new file mode 100644
index 00000000000..bca9aeb325c
--- /dev/null
+++ b/question/bank/deletequestion/amd/build/datafilter/filtertypes/hidden.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"hidden.min.js","sources":["../../../src/datafilter/filtertypes/hidden.js"],"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 .\n\n/**\n * Filter managing hidden questions.\n *\n * @module qbank_deletequestion/datafilter/filtertypes/hidden\n * @author 2022 Ghaly Marc-Alexandre \n * @copyright 2022 Catalyst IT Australia Pty Ltd\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Binary from 'core/datafilter/filtertypes/binary';\n\nexport default class extends Binary {\n}\n"],"names":["Binary"],"mappings":";;;;;;;;oKA0B6BA"}
\ No newline at end of file
diff --git a/mod/quiz/addrandomform.php b/question/bank/deletequestion/amd/src/datafilter/filtertypes/hidden.js
similarity index 63%
rename from mod/quiz/addrandomform.php
rename to question/bank/deletequestion/amd/src/datafilter/filtertypes/hidden.js
index df1c42fe9f1..5e2c9b5011b 100644
--- a/mod/quiz/addrandomform.php
+++ b/question/bank/deletequestion/amd/src/datafilter/filtertypes/hidden.js
@@ -1,4 +1,3 @@
-.
/**
- * File only retained to prevent fatal errors in code that tries to require/include this.
+ * Filter managing hidden questions.
*
- * @todo MDL-76612 delete this file as part of Moodle 4.6 development.
- * @deprecated This file is no longer required in Moodle 4.2+.
+ * @module qbank_deletequestion/datafilter/filtertypes/hidden
+ * @author 2022 Ghaly Marc-Alexandre
+ * @copyright 2022 Catalyst IT Australia Pty Ltd
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-defined('MOODLE_INTERNAL') || die();
-debugging('This file is no longer required in Moodle 4.2+. Please do not include/require it.', DEBUG_DEVELOPER);
+import Binary from 'core/datafilter/filtertypes/binary';
+
+export default class extends Binary {
+}
diff --git a/question/bank/deletequestion/classes/hidden_condition.php b/question/bank/deletequestion/classes/hidden_condition.php
new file mode 100644
index 00000000000..2e10b1723b9
--- /dev/null
+++ b/question/bank/deletequestion/classes/hidden_condition.php
@@ -0,0 +1,72 @@
+.
+
+namespace qbank_deletequestion;
+
+use core\output\datafilter;
+use core_question\local\bank\condition;
+
+use core_question\local\bank\question_version_status;
+
+/**
+ * This class controls whether hidden / deleted questions are hidden in the list.
+ *
+ * @package qbank_deletequestion
+ * @copyright 2013 Ray Morris
+ * @author 2021 Safat Shahin
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class hidden_condition extends condition {
+
+ public static function get_condition_key() {
+ return 'hidden';
+ }
+
+ /**
+ * Build query from filter value
+ *
+ * @param array $filter filter properties
+ * @return array where sql and params
+ */
+ public static function build_query_from_filter(array $filter): array {
+ $showhidden = (bool)$filter['values'][0];
+ $where = "";
+ $params = [];
+ if (!$showhidden) {
+ $where = "qv.status <> :hidden_condition";
+ $params = ['hidden_condition' => question_version_status::QUESTION_STATUS_HIDDEN];
+ }
+ return [$where, $params];
+ }
+
+ public function get_title() {
+ return get_string('showhidden', 'core_question');
+ }
+
+ public function get_join_list(): array {
+ return [
+ datafilter::JOINTYPE_ANY,
+ ];
+ }
+
+ public function get_filter_class() {
+ return 'qbank_deletequestion/datafilter/filtertypes/hidden';
+ }
+
+ public function is_required(): bool {
+ return true;
+ }
+}
diff --git a/question/bank/deletequestion/classes/plugin_feature.php b/question/bank/deletequestion/classes/plugin_feature.php
index f1b933810d8..65d1a35f472 100644
--- a/question/bank/deletequestion/classes/plugin_feature.php
+++ b/question/bank/deletequestion/classes/plugin_feature.php
@@ -27,6 +27,7 @@ namespace qbank_deletequestion;
use core_question\local\bank\bulk_action_base;
use core_question\local\bank\plugin_features_base;
+use core_question\local\bank\view;
/**
* Class columns is the entrypoint for the columns.
@@ -48,4 +49,10 @@ class plugin_feature extends plugin_features_base {
new bulk_delete_action(),
];
}
+
+ public function get_question_filters(view $qbank = null): array {
+ return [
+ new hidden_condition($qbank),
+ ];
+ }
}
diff --git a/question/bank/deletequestion/delete.php b/question/bank/deletequestion/delete.php
index 081ec3b6740..396dda83f99 100644
--- a/question/bank/deletequestion/delete.php
+++ b/question/bank/deletequestion/delete.php
@@ -106,10 +106,18 @@ if ($deleteselected) {
}
$questionlist = rtrim($questionlist, ',');
- $deleteurl = new \moodle_url('/question/bank/deletequestion/delete.php', [
- 'deleteselected' => $questionlist, 'deleteall' => $deleteall, 'confirm' => md5($questionlist),
- 'sesskey' => sesskey(), 'returnurl' => $returnurl, 'cmid' => $cmid, 'courseid' => $courseid,
- ]);
+ $deleteurl = new \moodle_url(
+ '/question/bank/deletequestion/delete.php',
+ [
+ 'deleteselected' => $questionlist,
+ 'deleteall' => $deleteall,
+ 'confirm' => md5($questionlist),
+ 'sesskey' => sesskey(),
+ 'returnurl' => $returnurl->out_as_local_url(false),
+ 'cmid' => $cmid,
+ 'courseid' => $courseid,
+ ],
+ );
$continue = new \single_button($deleteurl, get_string('delete'), 'post');
$questionids = explode(',', $questionlist);
diff --git a/question/templates/hidden_condition_advanced.mustache b/question/bank/deletequestion/templates/hidden_condition_advanced.mustache
similarity index 95%
rename from question/templates/hidden_condition_advanced.mustache
rename to question/bank/deletequestion/templates/hidden_condition_advanced.mustache
index cea0a0ced77..53f751e041b 100644
--- a/question/templates/hidden_condition_advanced.mustache
+++ b/question/bank/deletequestion/templates/hidden_condition_advanced.mustache
@@ -15,7 +15,7 @@
along with Moodle. If not, see .
}}
{{!
- @template core_question/hidden_condition_advanced
+ @template qbank_deletequestion/hidden_condition_advanced
Example context (json):
{
diff --git a/question/bank/deletequestion/tests/behat/delete_question_column.feature b/question/bank/deletequestion/tests/behat/delete_question_column.feature
index 11838054ae5..2e09b09a464 100644
--- a/question/bank/deletequestion/tests/behat/delete_question_column.feature
+++ b/question/bank/deletequestion/tests/behat/delete_question_column.feature
@@ -62,9 +62,7 @@ Feature: Use the qbank plugin manager page for deletequestion
| Tags | foo |
And I click on "Save changes" "button"
And I am on the "Test quiz" "mod_quiz > question bank" page
- And I open the autocomplete suggestions list
- And I click on "foo" item in the autocomplete list
- And "foo" "autocomplete_selection" should exist
+ And I apply question bank filter "Tag" with value "foo"
And I click on "First question" "checkbox"
And I click on "With selected" "button"
And I click on question bulk action "deleteselected"
diff --git a/question/bank/editquestion/tests/behat/question_status.feature b/question/bank/editquestion/tests/behat/question_status.feature
index e66622bf527..92fe44cfe12 100644
--- a/question/bank/editquestion/tests/behat/question_status.feature
+++ b/question/bank/editquestion/tests/behat/question_status.feature
@@ -20,7 +20,7 @@ Feature: Use the qbank base view to test the status change using
@javascript
Scenario: Question status dropdown should change the status of the question
Given I am on the "Test quiz" "mod_quiz > question bank" page logged in as "admin"
- And I set the field "Select a category" to "Test questions"
+ And I apply question bank filter "Category" with value "Test questions"
And I should see "Test questions"
And the field "question_status_dropdown" in the "First question" "table_row" matches value "Ready"
And the field "question_status_dropdown" in the "Second question" "table_row" matches value "Ready"
diff --git a/question/bank/history/classes/question_history_view.php b/question/bank/history/classes/question_history_view.php
index 4a984387787..2e61cb0bcd1 100644
--- a/question/bank/history/classes/question_history_view.php
+++ b/question/bank/history/classes/question_history_view.php
@@ -50,14 +50,22 @@ class question_history_view extends view {
* @param question_edit_contexts $contexts the contexts of api call
* @param moodle_url $pageurl url of the page
* @param stdClass $course course settings
- * @param int $entryid quiz settings
- * @param string $returnurl url to return to
+ * @param stdClass|null $cm (optional) activity settings.
+ * @param array $params the parameters required to initialize the api.
+ * @param array $extraparams any extra parameters need to initialized if the api is extended, it will be passed to js.
+ * @throws \moodle_exception
*/
- public function __construct(question_edit_contexts $contexts, moodle_url $pageurl, stdClass $course, int $entryid,
- string $returnurl) {
- parent::__construct($contexts, $pageurl, $course);
- $this->entryid = $entryid;
- $this->basereturnurl = new \moodle_url($returnurl);
+ public function __construct(
+ question_edit_contexts $contexts,
+ moodle_url $pageurl,
+ stdClass $course,
+ stdClass $cm = null,
+ array $params = [],
+ array $extraparams = [],
+ ) {
+ $this->entryid = $extraparams['entryid'];
+ $this->basereturnurl = new \moodle_url($extraparams['returnurl']);
+ parent::__construct($contexts, $pageurl, $course, $cm, $params, $extraparams);
}
protected function init_question_actions(): void {
@@ -78,31 +86,6 @@ class question_history_view extends view {
return $this->requiredcolumns;
}
- public function wanted_filters($cat, $tagids, $showhidden, $recurse, $editcontexts, $showquestiontext): void {
- $categorydata = explode(',', $cat);
- $contextid = $categorydata[1];
- $catcontext = \context::instance_by_id($contextid);
- $thiscontext = $this->get_most_specific_context();
- $this->display_question_bank_header();
-
- // Display tag filter if usetags setting is enabled/enablefilters is true.
- if ($this->enablefilters) {
- if (is_array($this->customfilterobjects)) {
- foreach ($this->customfilterobjects as $filterobjects) {
- $this->searchconditions[] = $filterobjects;
- }
- } else {
- if (get_config('core', 'usetags')) {
- array_unshift($this->searchconditions,
- new \core_question\bank\search\tag_condition([$catcontext, $thiscontext], $tagids));
- }
-
- array_unshift($this->searchconditions, new \core_question\bank\search\hidden_condition(!$showhidden));
- }
- }
- $this->display_options_form($showquestiontext);
- }
-
protected function display_advanced_search_form($advancedsearch): void {
foreach ($advancedsearch as $searchcondition) {
echo $searchcondition->display_options_adv();
@@ -133,9 +116,9 @@ class question_history_view extends view {
// Build the order by clause.
$sorts = [];
- foreach ($this->sort as $sort => $order) {
- list($colname, $subsort) = $this->parse_subsort($sort);
- $sorts[] = $this->requiredcolumns[$colname]->sort_expression($order < 0, $subsort);
+ foreach ($this->sort as $sortname => $sortorder) {
+ list($colname, $subsort) = $this->parse_subsort($sortname);
+ $sorts[] = $this->requiredcolumns[$colname]->sort_expression($sortorder == SORT_DESC, $subsort);
}
// Build the where clause.
diff --git a/question/bank/history/history.php b/question/bank/history/history.php
index 41ace41c54c..6e80d200765 100644
--- a/question/bank/history/history.php
+++ b/question/bank/history/history.php
@@ -34,10 +34,15 @@ $returnurl = required_param('returnurl', PARAM_LOCALURL);
list($thispageurl, $contexts, $cmid, $cm, $module, $pagevars) =
question_edit_setup('questions', '/question/bank/history/history.php');
-
+$pagevars['entryid'] = $entryid;
+$pagevars['returnurl'] = $returnurl;
$url = new moodle_url($thispageurl, ['entryid' => $entryid, 'returnurl' => $returnurl]);
$PAGE->set_url($url);
-$questionbank = new \qbank_history\question_history_view($contexts, $url, $COURSE, $entryid, $returnurl, $cm);
+
+// Additional param to differentiate with other question bank view.
+$extraparams['entryid'] = $entryid;
+$extraparams['returnurl'] = $returnurl;
+$questionbank = new \qbank_history\question_history_view($contexts, $url, $COURSE, null, $pagevars, $extraparams);
$streditingquestions = get_string('history_header', 'qbank_history');
$PAGE->set_title($streditingquestions);
@@ -49,5 +54,5 @@ $PAGE->navbar->add($streditingquestions, $url);
echo $OUTPUT->header();
// Print the question area.
-$questionbank->display($pagevars, 'questions');
+$questionbank->display();
echo $OUTPUT->footer();
diff --git a/question/bank/history/tests/behat/question_history_action.feature b/question/bank/history/tests/behat/question_history_action.feature
index 3011eeea9b6..cae416064ed 100644
--- a/question/bank/history/tests/behat/question_history_action.feature
+++ b/question/bank/history/tests/behat/question_history_action.feature
@@ -16,7 +16,6 @@ Feature: Use the qbank plugin manager page for question history
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | First question | Answer the first question |
- @javascript
Scenario: Enable/disable question history column 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
@@ -32,8 +31,6 @@ Feature: Use the qbank plugin manager page for question history
Scenario: History page shows only the specified features and questions
Given I am on the "Test quiz" "mod_quiz > question bank" page logged in as "admin"
And I choose "History" action for "First question" in the question bank
- Then I should not see "Select a category"
- And I should see "No tag filters applied"
And I should see "Question"
And I should see "Actions"
And I should see "Status"
diff --git a/question/bank/history/tests/question_history_view_test.php b/question/bank/history/tests/question_history_view_test.php
index 5c5c561c968..122a238d274 100644
--- a/question/bank/history/tests/question_history_view_test.php
+++ b/question/bank/history/tests/question_history_view_test.php
@@ -59,18 +59,22 @@ class question_history_view_test extends \advanced_testcase {
$entry = get_question_bank_entry($questiondata1->id);
- // Generate the view.
- $view = new question_history_view($contexts, new \moodle_url('/'), $course, $entry->id, '/');
- ob_start();
$pagevars = [
'qpage' => 0,
- 'qperpage' => 20,
+ 'qperpage' => DEFAULT_QUESTIONS_PER_PAGE,
'cat' => $cat->id . ',' . $cat->contextid,
- 'recurse' => false,
- 'showhidden' => false,
- 'qbshowtext' => false
+ 'tabname' => 'questions'
];
- $view->display($pagevars, 'questions');
+ // Generate the view.
+ $viewclass = \qbank_history\question_history_view::class;
+ $extraparams = [
+ 'view' => $viewclass,
+ 'entryid' => $entry->id,
+ 'returnurl' => "/",
+ ];
+ $view = new $viewclass($contexts, new \moodle_url('/'), $course, null, $pagevars, $extraparams);
+ ob_start();
+ $view->display();
$html = ob_get_clean();
// Verify the output includes the first version.
@@ -102,9 +106,20 @@ class question_history_view_test extends \advanced_testcase {
['name' => 'First version', 'category' => $cat->id]);
$entry = get_question_bank_entry($questiondata1->id);
-
+ $pagevars = [
+ 'qpage' => 0,
+ 'qperpage' => DEFAULT_QUESTIONS_PER_PAGE,
+ 'cat' => $cat->id . ',' . $cat->contextid,
+ 'tabname' => 'questions'
+ ];
// Generate the view.
- $view = new question_history_view($contexts, new \moodle_url('/'), $course, $entry->id, '/');
+ $viewclass = \qbank_history\question_history_view::class;
+ $extraparams = [
+ 'view' => $viewclass,
+ 'entryid' => $entry->id,
+ 'returnurl' => "/",
+ ];
+ $view = new $viewclass($contexts, new \moodle_url('/'), $course, null, $pagevars, $extraparams);
ob_start();
$view->display_question_bank_header();
$headerhtml = ob_get_clean();
@@ -113,7 +128,7 @@ class question_history_view_test extends \advanced_testcase {
$questiondata2 = $questiongenerator->update_question($questiondata1, null,
['name' => 'Second version']);
- $view = new question_history_view($contexts, new \moodle_url('/'), $course, $entry->id, new \moodle_url('/'));
+ $view = new $viewclass($contexts, new \moodle_url('/'), $course, null, $pagevars, $extraparams);
ob_start();
$view->display_question_bank_header();
$headerhtml = ob_get_clean();
diff --git a/question/bank/managecategories/amd/build/datafilter/filtertypes/categories.min.js b/question/bank/managecategories/amd/build/datafilter/filtertypes/categories.min.js
new file mode 100644
index 00000000000..d15698d8a19
--- /dev/null
+++ b/question/bank/managecategories/amd/build/datafilter/filtertypes/categories.min.js
@@ -0,0 +1,3 @@
+define("qbank_managecategories/datafilter/filtertypes/categories",["exports","core/datafilter/filtertype","core/templates"],(function(_exports,_filtertype,_templates){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_filtertype=_interopRequireDefault(_filtertype),_templates=_interopRequireDefault(_templates);class _default extends _filtertype.default{constructor(filterType,rootNode,initialValues){let filterOptions=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{includeSubcategories:!1};var obj,key,value;super(filterType,rootNode,initialValues),value={includeSubcategories:"input[name=category-subcategories]"},(key="SELECTORS")in(obj=this)?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,this.addSubcategoryCheckbox(filterOptions.includeSubcategories)}async addSubcategoryCheckbox(){let checked=arguments.length>0&&void 0!==arguments[0]&&arguments[0];const filterValueNode=this.getFilterValueNode(),{html:html}=await _templates.default.renderForPromise("qbank_managecategories/include_subcategories_checkbox",{checked:checked});filterValueNode.insertAdjacentHTML("afterend",html)}get filterOptions(){return[{name:"includesubcategories",value:this.filterRoot.querySelector(this.SELECTORS.includeSubcategories).checked}]}get filterValue(){return{name:this.name,jointype:this.jointype,values:this.values,filteroptions:this.filterOptions}}}return _exports.default=_default,_exports.default}));
+
+//# sourceMappingURL=categories.min.js.map
\ No newline at end of file
diff --git a/question/bank/managecategories/amd/build/datafilter/filtertypes/categories.min.js.map b/question/bank/managecategories/amd/build/datafilter/filtertypes/categories.min.js.map
new file mode 100644
index 00000000000..a5d11750061
--- /dev/null
+++ b/question/bank/managecategories/amd/build/datafilter/filtertypes/categories.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"categories.min.js","sources":["../../../src/datafilter/filtertypes/categories.js"],"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 .\n\n/**\n * Filter managing display of subcategories questions.\n *\n * @module qbank_managecategories/datafilter/filtertypes/categories\n * @author Mark Johnson \n * @copyright 2023 Catalyst IT Europe Ltd.\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport GenericFilter from 'core/datafilter/filtertype';\nimport Templates from 'core/templates';\n\nexport default class extends GenericFilter {\n\n SELECTORS = {\n includeSubcategories: 'input[name=category-subcategories]',\n };\n\n constructor(filterType, rootNode, initialValues, filterOptions = {includeSubcategories: false}) {\n super(filterType, rootNode, initialValues);\n this.addSubcategoryCheckbox(filterOptions.includeSubcategories);\n }\n\n async addSubcategoryCheckbox(checked = false) {\n const filterValueNode = this.getFilterValueNode();\n const {html} = await Templates.renderForPromise('qbank_managecategories/include_subcategories_checkbox', {\n checked: checked,\n });\n filterValueNode.insertAdjacentHTML('afterend', html);\n }\n\n get filterOptions() {\n return [\n {name: 'includesubcategories', value: this.filterRoot.querySelector(this.SELECTORS.includeSubcategories).checked}\n ];\n }\n\n get filterValue() {\n return {\n name: this.name,\n jointype: this.jointype,\n values: this.values,\n filteroptions: this.filterOptions,\n };\n }\n}\n"],"names":["GenericFilter","constructor","filterType","rootNode","initialValues","filterOptions","includeSubcategories","addSubcategoryCheckbox","checked","filterValueNode","this","getFilterValueNode","html","Templates","renderForPromise","insertAdjacentHTML","name","value","filterRoot","querySelector","SELECTORS","filterValue","jointype","values","filteroptions"],"mappings":"8bA2B6BA,oBAMzBC,YAAYC,WAAYC,SAAUC,mBAAeC,qEAAgB,CAACC,sBAAsB,2BAC9EJ,WAAYC,SAAUC,qBALpB,CACRE,qBAAsB,+KAKjBC,uBAAuBF,cAAcC,yDAGjBE,sEACnBC,gBAAkBC,KAAKC,sBACvBC,KAACA,YAAcC,mBAAUC,iBAAiB,wDAAyD,CACrGN,QAASA,UAEbC,gBAAgBM,mBAAmB,WAAYH,MAG/CP,0BACO,CACH,CAACW,KAAM,uBAAwBC,MAAOP,KAAKQ,WAAWC,cAAcT,KAAKU,UAAUd,sBAAsBE,UAI7Ga,wBACO,CACHL,KAAMN,KAAKM,KACXM,SAAUZ,KAAKY,SACfC,OAAQb,KAAKa,OACbC,cAAed,KAAKL"}
\ No newline at end of file
diff --git a/question/bank/managecategories/amd/src/datafilter/filtertypes/categories.js b/question/bank/managecategories/amd/src/datafilter/filtertypes/categories.js
new file mode 100644
index 00000000000..6061c9f7fa3
--- /dev/null
+++ b/question/bank/managecategories/amd/src/datafilter/filtertypes/categories.js
@@ -0,0 +1,61 @@
+// 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 .
+
+/**
+ * Filter managing display of subcategories questions.
+ *
+ * @module qbank_managecategories/datafilter/filtertypes/categories
+ * @author Mark Johnson
+ * @copyright 2023 Catalyst IT Europe Ltd.
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import GenericFilter from 'core/datafilter/filtertype';
+import Templates from 'core/templates';
+
+export default class extends GenericFilter {
+
+ SELECTORS = {
+ includeSubcategories: 'input[name=category-subcategories]',
+ };
+
+ constructor(filterType, rootNode, initialValues, filterOptions = {includeSubcategories: false}) {
+ super(filterType, rootNode, initialValues);
+ this.addSubcategoryCheckbox(filterOptions.includeSubcategories);
+ }
+
+ async addSubcategoryCheckbox(checked = false) {
+ const filterValueNode = this.getFilterValueNode();
+ const {html} = await Templates.renderForPromise('qbank_managecategories/include_subcategories_checkbox', {
+ checked: checked,
+ });
+ filterValueNode.insertAdjacentHTML('afterend', html);
+ }
+
+ get filterOptions() {
+ return [
+ {name: 'includesubcategories', value: this.filterRoot.querySelector(this.SELECTORS.includeSubcategories).checked}
+ ];
+ }
+
+ get filterValue() {
+ return {
+ name: this.name,
+ jointype: this.jointype,
+ values: this.values,
+ filteroptions: this.filterOptions,
+ };
+ }
+}
diff --git a/question/bank/managecategories/classes/category_condition.php b/question/bank/managecategories/classes/category_condition.php
new file mode 100644
index 00000000000..80c1a6bc17b
--- /dev/null
+++ b/question/bank/managecategories/classes/category_condition.php
@@ -0,0 +1,333 @@
+.
+
+namespace qbank_managecategories;
+
+use core\output\datafilter;
+use core_question\local\bank\condition;
+use core_question\local\bank\view;
+
+/**
+ * This class controls from which category questions are listed.
+ *
+ * @package qbank_managecategories
+ * @copyright 2013 Ray Morris
+ * @author 2021 Safat Shahin
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class category_condition extends condition {
+ /** @var \stdClass The course record. */
+ protected $course;
+
+ /** @var \stdClass The category record. */
+ protected $category;
+
+ /** @var array of contexts. */
+ protected $contexts;
+
+ /** @var string categoryID,contextID as used with question_bank_view->display(). */
+ protected $cat;
+
+ /** @var int The maximum displayed length of the category info. */
+ public $maxinfolength;
+
+ /** @var bool Include questions in subcategories of the specified category? */
+ public $includesubcategories;
+
+ /**
+ * Constructor to initialize the category filter condition.
+ *
+ * @param view $qbank qbank view
+ */
+ public function __construct(view $qbank = null) {
+ if (is_null($qbank)) {
+ return;
+ }
+ $this->cat = $qbank->get_pagevars('cat');
+ $this->contexts = $qbank->contexts->having_one_edit_tab_cap($qbank->get_pagevars('tabname'));
+ $this->course = $qbank->course;
+
+ [$categoryid, $contextid] = self::validate_category_param($this->cat);
+ if (is_null($categoryid)) {
+ return;
+ }
+
+ $this->category = self::get_category_record($categoryid, $contextid);
+
+ parent::__construct($qbank);
+ $this->includesubcategories = $this->filter['filteroptions']['includesubcategories'] ?? false;
+ }
+
+ /**
+ * Return default category
+ *
+ * @return \stdClass default category
+ */
+ public function get_default_category(): \stdClass {
+ if (empty($this->category)) {
+ return question_get_default_category(\context_course::instance($this->course->id)->id);
+ }
+
+ return $this->category;
+ }
+
+ public static function get_condition_key() {
+ return 'category';
+ }
+
+ /**
+ * Returns course id.
+ *
+ * @return string Course id.
+ */
+ public function get_course_id() {
+ return $this->course->id;
+ }
+
+ /**
+ * Called by question_bank_view to display the GUI for selecting a category
+ * @deprecated since Moodle 4.3 MDL-72321 - please do not use this function any more.
+ * @todo Final deprecation on Moodle 4.7 MDL-78090
+ */
+ public function display_options() {
+ debugging('Function display_options() is deprecated, please use filtering objects', DEBUG_DEVELOPER);
+ global $PAGE;
+ $displaydata = [];
+ $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'));
+ $displaydata['categorydesc'] = '';
+ if ($this->category) {
+ $displaydata['categorydesc'] = $this->print_category_info($this->category);
+ }
+ return $PAGE->get_renderer('qbank_managecategories')->render_category_condition($displaydata);
+ }
+
+ /**
+ * Displays the recursion checkbox GUI.
+ * question_bank_view places this within the section that is hidden by default
+ * @deprecated since Moodle 4.3 MDL-72321 - please do not use this function any more.
+ * @todo Final deprecation on Moodle 4.7 MDL-78090
+ */
+ public function display_options_adv() {
+ debugging('Function display_options_adv() is deprecated, please use filtering objects', DEBUG_DEVELOPER);
+ global $PAGE;
+ $displaydata = [];
+ if ($this->recurse) {
+ $displaydata['checked'] = 'checked';
+ }
+ return $PAGE->get_renderer('qbank_managecategories')->render_category_condition_advanced($displaydata);
+ }
+
+ /**
+ * Display the drop down to select the category.
+ *
+ * @param array $contexts of contexts that can be accessed from here.
+ * @param \moodle_url $pageurl the URL of this page.
+ * @param string $current 'categoryID,contextID'.
+ * @deprecated since Moodle 4.3
+ * @todo Final deprecation on Moodle 4.7 MDL-78090
+ */
+ protected function display_category_form($contexts, $pageurl, $current) {
+ debugging(
+ 'Function display_category_form() is deprecated, please use the core_question renderer instead.',
+ DEBUG_DEVELOPER
+ );
+ echo \html_writer::start_div('choosecategory');
+ $catmenu = question_category_options($contexts, true, 0, true, -1, false);
+ echo \html_writer::label(get_string('selectacategory', 'question'), 'id_selectacategory', true, ["class" => "mr-1"]);
+ echo \html_writer::select($catmenu, 'category', $current, [],
+ array('class' => 'searchoptions custom-select', 'id' => 'id_selectacategory'));
+ echo \html_writer::end_div() . "\n";
+ }
+
+ /**
+ * Print the text if category id not available.
+ * @deprecated since Moodle 4.3
+ * @todo Final deprecation in Moodle 4.7 MDL-78090
+ */
+ public static function print_choose_category_message(): void {
+ debugging(
+ 'Function print_choose_category_message() is deprecated, please use ' .
+ 'qbank_managecategories/choose_category template instead.',
+ DEBUG_DEVELOPER
+ );
+ global $OUTPUT;
+ echo $OUTPUT->render_from_template('qbank_managecategories/choose_category', []);
+ }
+
+ /**
+ * Look up the category record based on category ID and context
+ * @param string $categoryandcontext categoryID,contextID as used with question_bank_view->display()
+ * @return \stdClass The category record
+ * @deprecated since Moodle 4.3
+ * @todo Final deprecation in Moodle 4.7 MDL-78090
+ */
+ public static function get_current_category($categoryandcontext) {
+ debugging('Function get_current_category() is deprecated. Please do not use it anymore.', DEBUG_DEVELOPER);
+ global $DB, $OUTPUT;
+ [$categoryid, $contextid] = explode(',', $categoryandcontext);
+ if (!$categoryid) {
+ self::print_choose_category_message();
+ return false;
+ }
+
+ if (!$category = $DB->get_record('question_categories', ['id' => $categoryid, 'contextid' => $contextid])) {
+ echo $OUTPUT->box_start('generalbox questionbank');
+ echo $OUTPUT->notification('Category not found!');
+ echo $OUTPUT->box_end();
+ return false;
+ }
+
+ return $category;
+ }
+
+ /**
+ * Return category and context ID from compound parameter.
+ *
+ * @param string $categoryandcontext Comma-separated list of category and context IDs.
+ * @return int[]|null[]
+ */
+ public static function validate_category_param(string $categoryandcontext): array {
+ [$categoryid, $contextid] = explode(',', $categoryandcontext);
+ if (!$categoryid) {
+ return [null, null];
+ }
+ return [clean_param($categoryid, PARAM_INT), clean_param($contextid, PARAM_INT)];
+ }
+
+ /**
+ * Fetch the question category record matching the provided category and context IDs.
+ *
+ * @param int $categoryid
+ * @param int $contextid
+ * @return \stdClass
+ * @throws \dml_exception
+ */
+ public static function get_category_record($categoryid, $contextid): \stdClass {
+ global $DB;
+ return $DB->get_record('question_categories',
+ ['id' => $categoryid, 'contextid' => $contextid],
+ '*',
+ MUST_EXIST);
+ }
+
+ /**
+ * Print the category description
+ * @param \stdClass $category the category information form the database.
+ * @deprecated since Moodle 4.3 MDL-72321 - please do not use this function any more.
+ * @todo Final deprecation on Moodle 4.7 MDL-78090
+ */
+ protected function print_category_info($category): string {
+ debugging('Function print_category_info() is deprecated. Please do not use it anymore', DEBUG_DEVELOPER);
+ $formatoptions = new \stdClass();
+ $formatoptions->noclean = true;
+ $formatoptions->overflowdiv = true;
+ if (isset($this->maxinfolength)) {
+ return shorten_text(format_text($category->info, $category->infoformat, $formatoptions, $this->course->id),
+ $this->maxinfolength);
+ }
+
+ return format_text($category->info, $category->infoformat, $formatoptions, $this->course->id);
+ }
+
+ public static function build_query_from_filter(array $filter): array {
+ global $DB;
+ $recursive = false;
+ if (isset($filter['filteroptions']['includesubcategories'])) {
+ $recursive = (bool)$filter['filteroptions']['includesubcategories'];
+ }
+
+ // Sub categories.
+ if ($recursive) {
+ $categories = $filter['values'];
+ $categoriesandsubcategories = [];
+ foreach ($categories as $categoryid) {
+ $categoriesandsubcategories += question_categorylist($categoryid);
+ }
+ } else {
+ $categoriesandsubcategories = $filter['values'];
+ }
+
+ $jointype = $filter['jointype'] ?? self::JOINTYPE_DEFAULT;
+ $equal = !($jointype === datafilter::JOINTYPE_NONE);
+ [$insql, $params] = $DB->get_in_or_equal($categoriesandsubcategories, SQL_PARAMS_NAMED, 'cat', $equal);
+ $where = 'qbe.questioncategoryid ' . $insql;
+ return [$where, $params];
+ }
+
+ public function get_title() {
+ return get_string('category', 'core_question');
+ }
+
+ public function get_filter_class() {
+ return 'qbank_managecategories/datafilter/filtertypes/categories';
+ }
+
+ public function allow_custom() {
+ return false;
+ }
+
+ public function allow_multiple() {
+ return false;
+ }
+
+ public function allow_empty() {
+ return false;
+ }
+
+ public function get_join_list(): array {
+ return [
+ datafilter::JOINTYPE_ANY,
+ ];
+ }
+
+ public function get_initial_values() {
+ $catmenu = helper::question_category_options($this->contexts, true, 0, true, -1, false);
+ $values = [];
+ foreach ($catmenu as $menu) {
+ foreach ($menu as $heading => $catlist) {
+ $values[] = (object) [
+ // Add a list item for each question category context. This will serve as a "heading" within the list
+ // and we will use CSS to disable pointer events so it cannot be selected.
+ 'value' => '',
+ 'title' => $heading,
+ 'disabled' => true,
+ ];
+ foreach ($catlist as $key => $value) {
+ $values[] = (object) [
+ // Remove contextid from value.
+ 'value' => str_contains($key, ',') ? substr($key, 0, strpos($key, ',')) : $key,
+ 'title' => $value,
+ 'selected' => ($key === $this->cat),
+ ];
+ }
+ }
+ }
+ return $values;
+ }
+
+ public function get_filteroptions(): \stdClass {
+ return (object)[
+ 'includesubcategories' => $this->includesubcategories,
+ ];
+ }
+
+ public function is_required(): bool {
+ return true;
+ }
+}
diff --git a/question/bank/managecategories/classes/output/renderer.php b/question/bank/managecategories/classes/output/renderer.php
new file mode 100644
index 00000000000..a0129098ee3
--- /dev/null
+++ b/question/bank/managecategories/classes/output/renderer.php
@@ -0,0 +1,58 @@
+.
+
+namespace qbank_managecategories\output;
+
+use core\check\performance\debugging;
+use plugin_renderer_base;
+
+/**
+ * Class renderer for managecategories.
+ *
+ * @package qbank_managecategories
+ * @copyright 2021 Catalyst IT Australia Pty Ltd
+ * @author Ghaly Marc-Alexandre
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @deprecated since Moodle 4.3
+ * @todo Final deprecation on Moodle 4.7 MDL-78090
+ */
+class renderer extends plugin_renderer_base {
+ /**
+ * Render category condition advanced.
+ *
+ * @deprecated since Moodle 4.3
+ * @todo Final deprecation on Moodle 4.7 MDL-78090
+ * @param array $displaydata
+ * @return bool|string
+ */
+ public function render_category_condition_advanced($displaydata) {
+ debugging('render_category_condition_advanced() is deprecated. Use new filter objects instead.', DEBUG_DEVELOPER);
+ return $this->render_from_template('qbank_managecategories/category_condition_advanced', $displaydata);
+ }
+
+ /**
+ * Render category condition.
+ *
+ * @deprecated since Moodle 4.3
+ * @todo Final deprecation on Moodle 4.7 MDL-78090
+ * @param array $displaydata
+ * @return bool|string
+ */
+ public function render_category_condition($displaydata) {
+ debugging('render_category_condition() is deprecated. Use new filter objects instead.', DEBUG_DEVELOPER);
+ return $this->render_from_template('qbank_managecategories/category_condition', $displaydata);
+ }
+}
diff --git a/question/bank/managecategories/classes/plugin_feature.php b/question/bank/managecategories/classes/plugin_feature.php
index 81fc258e357..c182800887b 100644
--- a/question/bank/managecategories/classes/plugin_feature.php
+++ b/question/bank/managecategories/classes/plugin_feature.php
@@ -17,6 +17,8 @@
namespace qbank_managecategories;
use core_question\local\bank\navigation_node_base;
+use core_question\local\bank\plugin_features_base;
+use core_question\local\bank\view;
/**
* Class plugin_feature.
@@ -29,9 +31,15 @@ use core_question\local\bank\navigation_node_base;
* @author Safat Shahin
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-class plugin_feature extends \core_question\local\bank\plugin_features_base {
+class plugin_feature extends plugin_features_base {
public function get_navigation_node(): ?navigation_node_base {
return new navigation();
}
+
+ public function get_question_filters(view $qbank = null): array {
+ return [
+ new category_condition($qbank),
+ ];
+ }
}
diff --git a/question/templates/category_condition.mustache b/question/bank/managecategories/templates/category_condition.mustache
similarity index 78%
rename from question/templates/category_condition.mustache
rename to question/bank/managecategories/templates/category_condition.mustache
index 90cbc27d59a..e4e2f06c0c3 100644
--- a/question/templates/category_condition.mustache
+++ b/question/bank/managecategories/templates/category_condition.mustache
@@ -15,14 +15,17 @@
along with Moodle. If not, see .
}}
{{!
- @template core_question/category_condition
+ @template qbank_managecategories/category_condition
+
+ @deprecated since Moodle 4.3
+ @todo Final deprecation on Moodle 4.7 MDL-78090
Example context (json):
{
"displaydata": [
{
- "categoryselect": "html string",
- "categorydesc": "html string"
+ "categoryselect": " ",
+ "categorydesc": "Category description
"
}
]
}
diff --git a/question/templates/category_condition_advanced.mustache b/question/bank/managecategories/templates/category_condition_advanced.mustache
similarity index 88%
rename from question/templates/category_condition_advanced.mustache
rename to question/bank/managecategories/templates/category_condition_advanced.mustache
index ec5ac2f8042..1ea5d23de55 100644
--- a/question/templates/category_condition_advanced.mustache
+++ b/question/bank/managecategories/templates/category_condition_advanced.mustache
@@ -15,7 +15,10 @@
along with Moodle. If not, see .
}}
{{!
- @template core_question/category_condition_advanced
+ @template qbank_managecategories/category_condition_advanced
+
+ @deprecated since Moodle 4.3
+ @todo Final deprecation on Moodle 4.7 MDL-78090
Example context (json):
{
diff --git a/question/bank/managecategories/templates/choose_category.mustache b/question/bank/managecategories/templates/choose_category.mustache
new file mode 100644
index 00000000000..a7104f489fa
--- /dev/null
+++ b/question/bank/managecategories/templates/choose_category.mustache
@@ -0,0 +1,26 @@
+{{!
+ 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 .
+}}
+{{!
+ @template qbank_managecategories/choose_category
+
+ Example context (json):
+ {
+ }
+}}
+
+ {{#str}}selectcategoryabove, question{{/str}}
+
diff --git a/question/bank/managecategories/templates/include_subcategories_checkbox.mustache b/question/bank/managecategories/templates/include_subcategories_checkbox.mustache
new file mode 100644
index 00000000000..ab58b1f78d1
--- /dev/null
+++ b/question/bank/managecategories/templates/include_subcategories_checkbox.mustache
@@ -0,0 +1,33 @@
+{{!
+ 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 .
+}}
+{{!
+ @template qbank_managecategories/include_subcategories
+
+ Example context (json):
+ {
+ "displaydata": [
+ {
+ "categoryselect": "html string",
+ "categorydesc": "html string"
+ }
+ ]
+ }
+}}
+
+
+ {{#str}}includesubcategories, core_question{{/str}}
+
diff --git a/question/bank/managecategories/templates/listitem.mustache b/question/bank/managecategories/templates/listitem.mustache
index 934b002c9e2..aa6ed7feaac 100644
--- a/question/bank/managecategories/templates/listitem.mustache
+++ b/question/bank/managecategories/templates/listitem.mustache
@@ -19,6 +19,9 @@
This template renders the list item for each category.
+ @deprecated since Moodle 4.3
+ @todo Final deprecation on Moodle 4.7 MDL-78090
+
Example context (json):
{
"questionbankurl": "question/edit.php?cmid=",
diff --git a/question/bank/managecategories/tests/behat/question_categories.feature b/question/bank/managecategories/tests/behat/question_categories.feature
index f26939346c9..593323521cc 100644
--- a/question/bank/managecategories/tests/behat/question_categories.feature
+++ b/question/bank/managecategories/tests/behat/question_categories.feature
@@ -2,7 +2,7 @@
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
+ I create and edit categories, move questions between them and filter question using category filter
Background:
Given the following "users" exist:
@@ -15,15 +15,17 @@ Feature: A teacher can put questions in categories in the question bank
| 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 |
+ | contextlevel | reference | questioncategory | name |
+ | Course | C1 | Top | top |
+ | Course | C1 | top | Default for C1 |
+ | Course | C1 | Default for C1 | Subcategory |
+ | Course | C1 | Default for C1 | Another subcat |
+ | Course | C1 | top | Used category |
| Course | C1 | top | Default & testing |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Used category | essay | Test question to be moved | Write about whatever you want |
+ | Another subcat | essay | Question 1 | Write about whatever you want |
And I log in as "teacher1"
And I am on "Course 1" course homepage
@@ -89,3 +91,11 @@ Feature: A teacher can put questions in categories in the question bank
Then I should not see "Default for Test images in backup"
And I follow "Add category"
And I should see "Used category (2)"
+
+ Scenario: Filter questions by category and subcategories
+ When I am on the "Course 1" "core_question > course question bank" page
+ And I apply question bank filter "Category" with value "Default for C1"
+ Then I should not see "Question 1"
+ When I set the field "Also show questions from subcategories" to "1"
+ And I click on "Apply filters" "button"
+ Then I should see "Question 1" in the "categoryquestions" "table"
diff --git a/question/bank/managecategories/tests/behat/view_manage_categories_plugin.feature b/question/bank/managecategories/tests/behat/view_manage_categories_plugin.feature
index d50f3d288a9..cf6f5ce93e1 100644
--- a/question/bank/managecategories/tests/behat/view_manage_categories_plugin.feature
+++ b/question/bank/managecategories/tests/behat/view_manage_categories_plugin.feature
@@ -28,7 +28,7 @@ Feature: Use the qbank plugin manager page for managecategories
And I am on the "Test quiz" "mod_quiz > question bank" page
And I should see "Categories" in the "//div[contains(@class, 'urlselect')]//option[contains(text(), 'Categories')]" "xpath_element"
- Scenario: Enable/disable the tab New category when tyring to add a random question to a quiz
+ Scenario: Enable/disable the tab New category when trying 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"
@@ -38,7 +38,7 @@ Feature: Use the qbank plugin manager page for managecategories
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 click on "cancel" "button"
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
diff --git a/question/bank/managecategories/tests/helper_test.php b/question/bank/managecategories/tests/helper_test.php
index 6dd8e861954..a5c746a046a 100644
--- a/question/bank/managecategories/tests/helper_test.php
+++ b/question/bank/managecategories/tests/helper_test.php
@@ -16,9 +16,14 @@
namespace qbank_managecategories;
+defined('MOODLE_INTERNAL') || die();
+
use moodle_url;
use core_question\local\bank\question_edit_contexts;
+global $CFG;
+require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
+
/**
* Unit tests for helper class.
*
@@ -30,6 +35,8 @@ use core_question\local\bank\question_edit_contexts;
*/
class helper_test extends \advanced_testcase {
+ use \quiz_question_helper_test_trait;
+
/**
* @var \context_module module context.
*/
@@ -97,7 +104,7 @@ class helper_test extends \advanced_testcase {
quiz_add_quiz_question($q2b->id, $this->quiz);
// Adding a new random question does not add a new question, adds a question_set_references record.
- quiz_add_random_questions($this->quiz, 0, $qcat2->id, 1, false);
+ $this->add_random_questions($this->quiz->id, 0, $qcat2->id, 1);
// 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 qsr.*
diff --git a/question/bank/previewquestion/tests/behat/preview_question.feature b/question/bank/previewquestion/tests/behat/preview_question.feature
index 70bc0634113..883886ef92c 100644
--- a/question/bank/previewquestion/tests/behat/preview_question.feature
+++ b/question/bank/previewquestion/tests/behat/preview_question.feature
@@ -166,4 +166,4 @@ Feature: A teacher can preview questions in the question bank
Scenario: Question preview can be closed
And I press "Close preview"
Then I should not see "(latest)"
- And I should see "Course 1"
+ And I should see "Test quiz"
diff --git a/question/bank/statistics/tests/helper_test.php b/question/bank/statistics/tests/helper_test.php
index 671c896d5e7..8b77b3f2cb7 100644
--- a/question/bank/statistics/tests/helper_test.php
+++ b/question/bank/statistics/tests/helper_test.php
@@ -16,11 +16,16 @@
namespace qbank_statistics;
+defined('MOODLE_INTERNAL') || die();
+
use core_question\statistics\questions\all_calculated_for_qubaid_condition;
use mod_quiz\quiz_attempt;
use mod_quiz\quiz_settings;
use question_engine;
+global $CFG;
+require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
+
/**
* Tests for question statistics.
*
@@ -31,6 +36,8 @@ use question_engine;
*/
class helper_test extends \advanced_testcase {
+ use \quiz_question_helper_test_trait;
+
/**
* Test quizzes that contain a specified question.
*
@@ -100,7 +107,7 @@ class helper_test extends \advanced_testcase {
$this->assertEquals((object) ['component' => 'mod_quiz', 'contextid' => $quiz2context->id], $q2places[0]);
// Add a random question to quiz3.
- quiz_add_random_questions($quiz3, 0, $cat->id, 1, false);
+ $this->add_random_questions($quiz3->id, 0, $cat->id, 1);
$this->submit_quiz($quiz3, [1 => ['answer' => 'willbewrong']]);
// Quiz 3 will now be in one of these arrays.
diff --git a/question/bank/tagquestion/classes/output/renderer.php b/question/bank/tagquestion/classes/output/renderer.php
new file mode 100644
index 00000000000..1de360b2d12
--- /dev/null
+++ b/question/bank/tagquestion/classes/output/renderer.php
@@ -0,0 +1,39 @@
+.
+
+namespace qbank_tagquestion\output;
+
+use plugin_renderer_base;
+
+/**
+ * Class renderer for qbank_tagquestion.
+ *
+ * @package qbank_tagquestion
+ * @copyright 2021 Catalyst IT Australia Pty Ltd
+ * @author Ghaly Marc-Alexandre
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class renderer extends plugin_renderer_base {
+ /**
+ * Render tag condition.
+ *
+ * @param array $context
+ * @return bool|string
+ */
+ public function render_tag_condition($context) {
+ return $this->render_from_template('qbank_tagquestion/tag_condition', $context);
+ }
+}
diff --git a/question/bank/tagquestion/classes/plugin_feature.php b/question/bank/tagquestion/classes/plugin_feature.php
index ea24963cefd..e598fd9e0a9 100644
--- a/question/bank/tagquestion/classes/plugin_feature.php
+++ b/question/bank/tagquestion/classes/plugin_feature.php
@@ -17,7 +17,7 @@
namespace qbank_tagquestion;
use core_question\local\bank\plugin_features_base;
-
+use core_question\local\bank\view;
/**
* Class columns is the entrypoint for the columns.
*
@@ -33,4 +33,13 @@ class plugin_feature extends plugin_features_base{
new tags_action($qbank),
];
}
+
+ public function get_question_filters(view $qbank = null): array {
+ global $CFG;
+ if ($CFG->usetags) {
+ return [
+ new tag_condition($qbank),
+ ];
+ }
+ }
}
diff --git a/question/bank/tagquestion/classes/tag_condition.php b/question/bank/tagquestion/classes/tag_condition.php
new file mode 100644
index 00000000000..161d52e34da
--- /dev/null
+++ b/question/bank/tagquestion/classes/tag_condition.php
@@ -0,0 +1,168 @@
+.
+
+namespace qbank_tagquestion;
+
+use core\output\datafilter;
+use core_question\local\bank\condition;
+
+/**
+ * Question bank search class to allow searching/filtering by tags on a question.
+ *
+ * @package qbank_tagquestion
+ * @copyright 2018 Ryan Wyllie
+ * @author 2021 Safat Shahin
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tag_condition extends condition {
+
+ /** @var int The default filter type */
+ const JOINTYPE_DEFAULT = datafilter::JOINTYPE_ALL;
+
+ /** @var array Contexts to be used. */
+ protected $contexts = [];
+
+ /** @var array List of IDs for tags that have been selected in the form. */
+ protected $selectedtagids = [];
+
+ /**
+ * Tag condition constructor. It uses the qbank object and initialises all the its required information
+ * to be passed as a part of condition to get the questions.
+ *
+ * @param null $qbank qbank view
+ */
+ public function __construct($qbank = null) {
+ if (is_null($qbank)) {
+ return;
+ }
+ parent::__construct($qbank);
+ $cat = $qbank->get_pagevars('cat');
+ if (is_array($cat)) {
+ foreach ($cat as $value) {
+ [, $contextid] = explode(',', $value);
+ $catcontext = \context::instance_by_id($contextid);
+ $this->contexts[] = $catcontext;
+ }
+ } else {
+ [, $contextid] = explode(',', $qbank->get_pagevars('cat'));
+ $catcontext = \context::instance_by_id($contextid);
+ $this->contexts[] = $catcontext;
+ }
+ $thiscontext = $qbank->get_most_specific_context();
+ $this->contexts[] = $thiscontext;
+ $this->selectedtagids = $this->filter->values ?? [];
+ }
+
+ public static function get_condition_key() {
+ return 'qtagids';
+ }
+
+ /**
+ * Print HTML to display the list of tags to filter by.
+ */
+ public function display_options() {
+ global $PAGE;
+
+ $tags = \core_tag_tag::get_tags_by_area_in_contexts('core_question', 'question', $this->contexts);
+ $tagoptions = array_map(function($tag) {
+ return [
+ 'id' => $tag->id,
+ 'name' => $tag->name,
+ 'selected' => in_array($tag->id, $this->selectedtagids)
+ ];
+ }, array_values($tags));
+ $context = [
+ 'tagoptions' => $tagoptions
+ ];
+
+ return $PAGE->get_renderer('qbank_tagquestion')->render_tag_condition($context);
+ }
+
+ /**
+ * Build query from filter value
+ *
+ * @param array $filter filter properties
+ * @return array where sql and params
+ */
+ public static function build_query_from_filter(array $filter): array {
+ global $DB;
+
+ // Remove empty string.
+ $filter['values'] = array_filter($filter['values']);
+
+ $selectedtagids = $filter['values'] ?? [];
+
+ $params = [];
+ $where = '';
+ $jointype = $filter['jointype'] ?? self::JOINTYPE_DEFAULT;
+ // If some tags have been selected then we need to filter
+ // the question list by the selected tags.
+ if ($selectedtagids) {
+ // We treat each additional tag as an AND condition rather than
+ // an OR condition.
+ //
+ // For example, if the user filters by the tags "foo" and "bar" then
+ // we reduce the question list to questions that are tagged with both
+ // "foo" AND "bar". Any question that does not have ALL of the specified
+ // tags will be omitted.
+ $equal = !($jointype === datafilter::JOINTYPE_NONE);
+ [$tagsql, $tagparams] = $DB->get_in_or_equal($selectedtagids, SQL_PARAMS_NAMED, 'param', $equal);
+ $tagparams['tagcount'] = count($selectedtagids);
+ $tagparams['questionitemtype'] = 'question';
+ $tagparams['questioncomponent'] = 'core_question';
+ $params = $tagparams;
+ $where = "q.id IN (SELECT ti.itemid
+ FROM {tag_instance} ti
+ WHERE ti.itemtype = :questionitemtype
+ AND ti.component = :questioncomponent
+ AND ti.tagid {$tagsql}
+ GROUP BY ti.itemid ";
+ if ($jointype === datafilter::JOINTYPE_ALL) {
+ $where .= "HAVING COUNT(itemid) = :tagcount ";
+ }
+ $where .= ") ";
+
+ }
+
+ return [$where, $params];
+ }
+
+
+ public function get_title() {
+ return get_string('tag', 'tag');
+ }
+
+ public function get_filter_class() {
+ return null;
+ }
+
+ public function allow_custom() {
+ return false;
+ }
+
+ public function get_initial_values() {
+ $tags = \core_tag_tag::get_tags_by_area_in_contexts('core_question', 'question', $this->contexts);
+ $values = [];
+ foreach ($tags as $tag) {
+ $values[] = [
+ 'value' => $tag->id,
+ 'title' => html_entity_decode($tag->name),
+ 'selected' => in_array($tag->id, $this->selectedtagids)
+ ];
+ }
+ return $values;
+ }
+}
diff --git a/question/templates/tag_condition.mustache b/question/bank/tagquestion/templates/tag_condition.mustache
similarity index 98%
rename from question/templates/tag_condition.mustache
rename to question/bank/tagquestion/templates/tag_condition.mustache
index 41e25ae16ab..76a50f20f0f 100644
--- a/question/templates/tag_condition.mustache
+++ b/question/bank/tagquestion/templates/tag_condition.mustache
@@ -15,7 +15,7 @@
along with Moodle. If not, see .
}}
{{!
- @template core_question/tag_condition
+ @template qbank_tagquestion/tag_condition
An auto-complete select box containing a list of available tags to
filter the quesiton bank questions by.
diff --git a/question/bank/usage/amd/build/usage.min.js b/question/bank/usage/amd/build/usage.min.js
index a69dca4a123..d544be6d8d9 100644
--- a/question/bank/usage/amd/build/usage.min.js
+++ b/question/bank/usage/amd/build/usage.min.js
@@ -6,6 +6,6 @@ define("qbank_usage/usage",["exports","core/fragment","core/modal_factory","core
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Safat Shahin
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_fragment=_interopRequireDefault(_fragment),_modal_factory=_interopRequireDefault(_modal_factory),_notification=_interopRequireDefault(_notification),Str=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Str);let modal=null;const usageEvent=async(questionId,contextId,specificVersion)=>{const args={questionid:questionId,specificversion:specificVersion};if(null===modal){try{modal=await _modal_factory.default.create({type:_modal_factory.default.types.CANCEL,title:Str.get_string("usageheader","qbank_usage"),body:_fragment.default.loadFragment("qbank_usage","question_usage",contextId,args),large:!0})}catch(e){return void _notification.default.exception(e)}modal.show(),modal.getRoot().on("click","a[href].page-link",(function(e){e.preventDefault();let attr=e.target.getAttribute("href");"#"!==attr&&(args.querystring=attr,modal.setBody(_fragment.default.loadFragment("qbank_usage","question_usage",contextId,args)))})),modal.getRoot().on("change","#question_usage_version_dropdown",(function(e){args.questionid=e.target.value,modal.setBody(_fragment.default.loadFragment("qbank_usage","question_usage",contextId,args))}))}else modal.setBody(_fragment.default.loadFragment("qbank_usage","question_usage",contextId,args)),modal.show()};_exports.init=function(questionSelector,contextId){let specificVersion=arguments.length>2&&void 0!==arguments[2]&&arguments[2],target=document.querySelector(questionSelector),questionId=target.getAttribute("data-questionid");target.addEventListener("click",(()=>{usageEvent(questionId,contextId,specificVersion)}))}}));
+ */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_fragment=_interopRequireDefault(_fragment),_modal_factory=_interopRequireDefault(_modal_factory),_notification=_interopRequireDefault(_notification),Str=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Str);let modal=null;const usageEvent=async(questionId,contextId,specificVersion)=>{const args={questionid:questionId,specificversion:specificVersion};if(null===modal){try{modal=await _modal_factory.default.create({type:_modal_factory.default.types.CANCEL,title:Str.get_string("usageheader","qbank_usage"),body:_fragment.default.loadFragment("qbank_usage","question_usage",contextId,args),large:!0})}catch(e){return void _notification.default.exception(e)}modal.show(),modal.getRoot().on("click","a[href].page-link",(function(e){e.preventDefault();let attr=e.target.getAttribute("href");"#"!==attr&&(args.querystring=attr,modal.setBody(_fragment.default.loadFragment("qbank_usage","question_usage",contextId,args)))})),modal.getRoot().on("change","#question_usage_version_dropdown",(function(e){args.questionid=e.target.value,modal.setBody(_fragment.default.loadFragment("qbank_usage","question_usage",contextId,args))}))}else modal.setBody(_fragment.default.loadFragment("qbank_usage","question_usage",contextId,args)),modal.show()};_exports.init=function(){let specificVersion=arguments.length>0&&void 0!==arguments[0]&&arguments[0];const target=document.querySelector("#categoryquestions");null!==target&&target.addEventListener("click",(e=>{e.target.dataset.target&&e.target.dataset.target.includes("questionusagepreview")&&usageEvent(e.target.dataset.questionid,e.target.dataset.contextid,specificVersion)}))}}));
//# sourceMappingURL=usage.min.js.map
\ No newline at end of file
diff --git a/question/bank/usage/amd/build/usage.min.js.map b/question/bank/usage/amd/build/usage.min.js.map
index 53b2a3c217f..b34b7215b92 100644
--- a/question/bank/usage/amd/build/usage.min.js.map
+++ b/question/bank/usage/amd/build/usage.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"usage.min.js","sources":["../src/usage.js"],"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 .\n\n/**\n * Usage column selector js.\n *\n * @module qbank_usage/usage\n * @copyright 2021 Catalyst IT Australia Pty Ltd\n * @author Safat Shahin \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Fragment from 'core/fragment';\nimport ModalFactory from 'core/modal_factory';\nimport Notification from 'core/notification';\nimport * as Str from 'core/str';\n\nlet modal = null;\n\n/**\n * Event listeners for the module.\n *\n * @method clickEvent\n * @param {int} questionId\n * @param {int} contextId\n * @param {boolean} specificVersion Is the view listing specific question versions?\n */\nconst usageEvent = async(questionId, contextId, specificVersion) => {\n const args = {\n questionid: questionId,\n specificversion: specificVersion,\n };\n if (modal === null) {\n try {\n modal = await ModalFactory.create({\n type: ModalFactory.types.CANCEL,\n title: Str.get_string('usageheader', 'qbank_usage'),\n body: Fragment.loadFragment('qbank_usage', 'question_usage', contextId, args),\n large: true,\n });\n } catch (e) {\n Notification.exception(e);\n return;\n }\n\n modal.show();\n modal.getRoot().on('click', 'a[href].page-link', function(e) {\n e.preventDefault();\n let attr = e.target.getAttribute(\"href\");\n if (attr !== '#') {\n args.querystring = attr;\n modal.setBody(Fragment.loadFragment('qbank_usage', 'question_usage', contextId, args));\n }\n });\n // Version selection event.\n modal.getRoot().on('change', '#question_usage_version_dropdown', function(e) {\n args.questionid = e.target.value;\n modal.setBody(Fragment.loadFragment('qbank_usage', 'question_usage', contextId, args));\n });\n } else {\n modal.setBody(Fragment.loadFragment('qbank_usage', 'question_usage', contextId, args));\n modal.show();\n }\n\n};\n\n/**\n * Entrypoint of the js.\n *\n * @method init\n * @param {string} questionSelector the question usage identifier.\n * @param {int} contextId the question context id.\n * @param {boolean} specificVersion Is the view listing specific question versions?\n */\nexport const init = (questionSelector, contextId, specificVersion = false) => {\n let target = document.querySelector(questionSelector);\n let questionId = target.getAttribute('data-questionid');\n target.addEventListener('click', () => {\n // Call for the event listener to listed for clicks in any usage count row.\n usageEvent(questionId, contextId, specificVersion);\n });\n};\n"],"names":["modal","usageEvent","async","questionId","contextId","specificVersion","args","questionid","specificversion","ModalFactory","create","type","types","CANCEL","title","Str","get_string","body","Fragment","loadFragment","large","e","exception","show","getRoot","on","preventDefault","attr","target","getAttribute","querystring","setBody","value","questionSelector","document","querySelector","addEventListener"],"mappings":";;;;;;;;k4BA6BIA,MAAQ,WAUNC,WAAaC,MAAMC,WAAYC,UAAWC,yBACtCC,KAAO,CACTC,WAAYJ,WACZK,gBAAiBH,oBAEP,OAAVL,MAAgB,KAEZA,YAAcS,uBAAaC,OAAO,CAC9BC,KAAMF,uBAAaG,MAAMC,OACzBC,MAAOC,IAAIC,WAAW,cAAe,eACrCC,KAAMC,kBAASC,aAAa,cAAe,iBAAkBf,UAAWE,MACxEc,OAAO,IAEb,MAAOC,qCACQC,UAAUD,GAI3BrB,MAAMuB,OACNvB,MAAMwB,UAAUC,GAAG,QAAS,qBAAqB,SAASJ,GACtDA,EAAEK,qBACEC,KAAON,EAAEO,OAAOC,aAAa,QACpB,MAATF,OACArB,KAAKwB,YAAcH,KACnB3B,MAAM+B,QAAQb,kBAASC,aAAa,cAAe,iBAAkBf,UAAWE,WAIxFN,MAAMwB,UAAUC,GAAG,SAAU,oCAAoC,SAASJ,GACtEf,KAAKC,WAAac,EAAEO,OAAOI,MAC3BhC,MAAM+B,QAAQb,kBAASC,aAAa,cAAe,iBAAkBf,UAAWE,eAGpFN,MAAM+B,QAAQb,kBAASC,aAAa,cAAe,iBAAkBf,UAAWE,OAChFN,MAAMuB,sBAaM,SAACU,iBAAkB7B,eAAWC,wEAC1CuB,OAASM,SAASC,cAAcF,kBAChC9B,WAAayB,OAAOC,aAAa,mBACrCD,OAAOQ,iBAAiB,SAAS,KAE7BnC,WAAWE,WAAYC,UAAWC"}
\ No newline at end of file
+{"version":3,"file":"usage.min.js","sources":["../src/usage.js"],"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 .\n\n/**\n * Usage column selector js.\n *\n * @module qbank_usage/usage\n * @copyright 2021 Catalyst IT Australia Pty Ltd\n * @author Safat Shahin \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Fragment from 'core/fragment';\nimport ModalFactory from 'core/modal_factory';\nimport Notification from 'core/notification';\nimport * as Str from 'core/str';\n\nlet modal = null;\n\n/**\n * Event listeners for the module.\n *\n * @method clickEvent\n * @param {int} questionId\n * @param {int} contextId\n * @param {boolean} specificVersion Is the view listing specific question versions?\n */\nconst usageEvent = async(questionId, contextId, specificVersion) => {\n const args = {\n questionid: questionId,\n specificversion: specificVersion,\n };\n if (modal === null) {\n try {\n modal = await ModalFactory.create({\n type: ModalFactory.types.CANCEL,\n title: Str.get_string('usageheader', 'qbank_usage'),\n body: Fragment.loadFragment('qbank_usage', 'question_usage', contextId, args),\n large: true,\n });\n } catch (e) {\n Notification.exception(e);\n return;\n }\n\n modal.show();\n modal.getRoot().on('click', 'a[href].page-link', function(e) {\n e.preventDefault();\n let attr = e.target.getAttribute(\"href\");\n if (attr !== '#') {\n args.querystring = attr;\n modal.setBody(Fragment.loadFragment('qbank_usage', 'question_usage', contextId, args));\n }\n });\n // Version selection event.\n modal.getRoot().on('change', '#question_usage_version_dropdown', function(e) {\n args.questionid = e.target.value;\n modal.setBody(Fragment.loadFragment('qbank_usage', 'question_usage', contextId, args));\n });\n } else {\n modal.setBody(Fragment.loadFragment('qbank_usage', 'question_usage', contextId, args));\n modal.show();\n }\n\n};\n\n/**\n * Entrypoint of the js.\n *\n * @method init\n * @param {boolean} specificVersion Is the view listing specific question versions?\n */\nexport const init = (specificVersion = false) => {\n const target = document.querySelector('#categoryquestions');\n if (target !== null) {\n target.addEventListener('click', (e) => {\n if (e.target.dataset.target && e.target.dataset.target.includes('questionusagepreview')) {\n // Call for the event listener to listed for clicks in any usage count row.\n usageEvent(e.target.dataset.questionid, e.target.dataset.contextid, specificVersion);\n }\n });\n }\n};\n"],"names":["modal","usageEvent","async","questionId","contextId","specificVersion","args","questionid","specificversion","ModalFactory","create","type","types","CANCEL","title","Str","get_string","body","Fragment","loadFragment","large","e","exception","show","getRoot","on","preventDefault","attr","target","getAttribute","querystring","setBody","value","document","querySelector","addEventListener","dataset","includes","contextid"],"mappings":";;;;;;;;k4BA6BIA,MAAQ,WAUNC,WAAaC,MAAMC,WAAYC,UAAWC,yBACtCC,KAAO,CACTC,WAAYJ,WACZK,gBAAiBH,oBAEP,OAAVL,MAAgB,KAEZA,YAAcS,uBAAaC,OAAO,CAC9BC,KAAMF,uBAAaG,MAAMC,OACzBC,MAAOC,IAAIC,WAAW,cAAe,eACrCC,KAAMC,kBAASC,aAAa,cAAe,iBAAkBf,UAAWE,MACxEc,OAAO,IAEb,MAAOC,qCACQC,UAAUD,GAI3BrB,MAAMuB,OACNvB,MAAMwB,UAAUC,GAAG,QAAS,qBAAqB,SAASJ,GACtDA,EAAEK,qBACEC,KAAON,EAAEO,OAAOC,aAAa,QACpB,MAATF,OACArB,KAAKwB,YAAcH,KACnB3B,MAAM+B,QAAQb,kBAASC,aAAa,cAAe,iBAAkBf,UAAWE,WAIxFN,MAAMwB,UAAUC,GAAG,SAAU,oCAAoC,SAASJ,GACtEf,KAAKC,WAAac,EAAEO,OAAOI,MAC3BhC,MAAM+B,QAAQb,kBAASC,aAAa,cAAe,iBAAkBf,UAAWE,eAGpFN,MAAM+B,QAAQb,kBAASC,aAAa,cAAe,iBAAkBf,UAAWE,OAChFN,MAAMuB,sBAWM,eAAClB,8EACXuB,OAASK,SAASC,cAAc,sBACvB,OAAXN,QACAA,OAAOO,iBAAiB,SAAUd,IAC1BA,EAAEO,OAAOQ,QAAQR,QAAUP,EAAEO,OAAOQ,QAAQR,OAAOS,SAAS,yBAE5DpC,WAAWoB,EAAEO,OAAOQ,QAAQ7B,WAAYc,EAAEO,OAAOQ,QAAQE,UAAWjC"}
\ No newline at end of file
diff --git a/question/bank/usage/amd/src/usage.js b/question/bank/usage/amd/src/usage.js
index a2a8af6e4cf..51838e5d1d3 100644
--- a/question/bank/usage/amd/src/usage.js
+++ b/question/bank/usage/amd/src/usage.js
@@ -80,15 +80,16 @@ const usageEvent = async(questionId, contextId, specificVersion) => {
* Entrypoint of the js.
*
* @method init
- * @param {string} questionSelector the question usage identifier.
- * @param {int} contextId the question context id.
* @param {boolean} specificVersion Is the view listing specific question versions?
*/
-export const init = (questionSelector, contextId, specificVersion = false) => {
- let target = document.querySelector(questionSelector);
- let questionId = target.getAttribute('data-questionid');
- target.addEventListener('click', () => {
- // Call for the event listener to listed for clicks in any usage count row.
- usageEvent(questionId, contextId, specificVersion);
- });
+export const init = (specificVersion = false) => {
+ const target = document.querySelector('#categoryquestions');
+ if (target !== null) {
+ target.addEventListener('click', (e) => {
+ if (e.target.dataset.target && e.target.dataset.target.includes('questionusagepreview')) {
+ // Call for the event listener to listed for clicks in any usage count row.
+ usageEvent(e.target.dataset.questionid, e.target.dataset.contextid, specificVersion);
+ }
+ });
+ }
};
diff --git a/question/bank/usage/classes/question_usage_column.php b/question/bank/usage/classes/question_usage_column.php
index 955708a03f3..66ff16285a0 100644
--- a/question/bank/usage/classes/question_usage_column.php
+++ b/question/bank/usage/classes/question_usage_column.php
@@ -28,6 +28,19 @@ use core_question\local\bank\column_base;
*/
class question_usage_column extends column_base {
+ /**
+ * Include Javascript module.
+ *
+ * @return void
+ */
+ public function init(): void {
+ parent::init();
+ global $PAGE;
+ $PAGE->requires->js_call_amd('qbank_usage/usage', 'init', [
+ $this->qbank->is_listing_specific_versions()
+ ]);
+ }
+
public function get_name(): string {
return 'questionusage';
}
@@ -41,22 +54,16 @@ class question_usage_column extends column_base {
}
protected function display_content($question, $rowclasses): void {
- global $PAGE;
$usagecount = helper::get_question_entry_usage_count($question, $this->qbank->is_listing_specific_versions());
$attributes = [];
if (question_has_capability_on($question, 'view')) {
$target = 'questionusagepreview_' . $question->id;
- $datatarget = '[data-target="' . $target . '"]';
- $PAGE->requires->js_call_amd('qbank_usage/usage', 'init', [
- $datatarget,
- $question->contextid,
- $this->qbank->is_listing_specific_versions(),
- ]);
$attributes = [
'href' => '#',
'data-target' => $target,
'data-questionid' => $question->id,
'data-courseid' => $this->qbank->course->id,
+ 'data-contextid' => $question->contextid,
];
}
echo \html_writer::tag('a', $usagecount, $attributes);
diff --git a/question/bank/usage/tests/behat/question_usage_column.feature b/question/bank/usage/tests/behat/question_usage_column.feature
index 5438ee91d82..39cc9dd6174 100644
--- a/question/bank/usage/tests/behat/question_usage_column.feature
+++ b/question/bank/usage/tests/behat/question_usage_column.feature
@@ -31,7 +31,7 @@ Feature: Use the qbank plugin manager page for question usage
@javascript
Scenario: Question usage modal should work without any usage data
And I am on the "Test quiz" "mod_quiz > question bank" page logged in as "admin"
- And I set the field "Select a category" to "Test questions"
+ And I apply question bank filter "Category" with value "Test questions"
And I should see "Test questions"
And I should see "0" on the usage column
When I click "0" on the usage column
@@ -46,7 +46,7 @@ Feature: Use the qbank plugin manager page for question usage
| question | page |
| First question | 1 |
And I am on the "Test quiz" "mod_quiz > question bank" page logged in as "admin"
- And I set the field "Select a category" to "Test questions"
+ And I apply question bank filter "Category" with value "Test questions"
And I should see "Test questions"
And I should see "1" on the usage column
When I click "1" on the usage column
diff --git a/question/bank/usage/tests/helper_test.php b/question/bank/usage/tests/helper_test.php
index 77cef5fdd0f..efab696af7c 100644
--- a/question/bank/usage/tests/helper_test.php
+++ b/question/bank/usage/tests/helper_test.php
@@ -16,8 +16,13 @@
namespace qbank_usage;
+defined('MOODLE_INTERNAL') || die();
+
use mod_quiz\quiz_attempt;
+global $CFG;
+require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
+
/**
* Helper test.
*
@@ -29,6 +34,8 @@ use mod_quiz\quiz_attempt;
*/
class helper_test extends \advanced_testcase {
+ use \quiz_question_helper_test_trait;
+
/**
* @var \stdClass $quiz
*/
@@ -133,7 +140,7 @@ class helper_test extends \advanced_testcase {
$this->setAdminUser();
$cat = $this->questiongenerator->create_question_category();
$question = $this->questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
- quiz_add_random_questions($this->quiz, 1, $cat->id, 1, false);
+ $this->add_random_questions($this->quiz->id, 1, $cat->id, 1);
$qdef = \question_bank::load_question($question->id);
$count = helper::get_question_entry_usage_count($qdef);
@@ -226,7 +233,7 @@ class helper_test extends \advanced_testcase {
$this->setAdminUser();
$cat = $this->questiongenerator->create_question_category();
$question = $this->questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
- quiz_add_random_questions($this->quiz, 1, $cat->id, 1, false);
+ $this->add_random_questions($this->quiz->id, 1, $cat->id, 1);
$this->attempt_quiz();
diff --git a/question/bank/viewcreator/tests/behat/modified_by_column.feature b/question/bank/viewcreator/tests/behat/modified_by_column.feature
index f478aa63f80..abefc6e49c3 100644
--- a/question/bank/viewcreator/tests/behat/modified_by_column.feature
+++ b/question/bank/viewcreator/tests/behat/modified_by_column.feature
@@ -33,19 +33,25 @@ Feature: Question bank Modified by column
And I should see "View creator"
When I click on "Disable" "link" in the "View creator" "table_row"
And I am on the "Test quiz" "mod_quiz > question bank" page
- And I set the field "Select a category" to "Test questions"
+ And I set the field "Filter type" to "Category"
+ And I set the field "Category" to "Test questions"
+ And I press "Apply filters"
Then I should not see "Modified by"
And I navigate to "Plugins > Question bank plugins > Manage question bank plugins" in site administration
And I click on "Enable" "link" in the "View creator" "table_row"
And I am on the "Test quiz" "mod_quiz > question bank" page
- And I set the field "Select a category" to "Test questions"
+ And I set the field "Filter type" to "Category"
+ And I set the field "Category" to "Test questions"
+ And I press "Apply filters"
Then I should see "Modified by"
@javascript
Scenario: Editing a question shows the modifier of the question
And I log in as "teacher1"
And I am on the "Test quiz" "mod_quiz > question bank" page
- And I set the field "Select a category" to "Test questions"
+ And I set the field "Filter type" to "Category"
+ And I set the field "Category" to "Test questions"
+ And I press "Apply filters"
And I should see "First question"
And I choose "Edit question" action for "First question" in the question bank
And I should see "Version 1"
@@ -55,7 +61,9 @@ Feature: Question bank Modified by column
And I should see "Teacher1"
And I log in as "teacher2"
And I am on the "Test quiz" "mod_quiz > question bank" page
- And I set the field "Select a category" to "Test questions"
+ And I set the field "Filter type" to "Category"
+ And I set the field "Category" to "Test questions"
+ And I press "Apply filters"
And I choose "Edit question" action for "Renamed question v2" in the question bank
Then I should see "Version 2"
And I set the field "id_name" to "Renamed question v3"
diff --git a/question/bank/viewquestionname/tests/behat/question_in_place_editing.feature b/question/bank/viewquestionname/tests/behat/question_in_place_editing.feature
index 8cdbaa4e420..0fcfd19a8af 100644
--- a/question/bank/viewquestionname/tests/behat/question_in_place_editing.feature
+++ b/question/bank/viewquestionname/tests/behat/question_in_place_editing.feature
@@ -21,20 +21,22 @@ Feature: Use the qbank view page to edit question title using in place edit feat
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | First question | Answer the first question |
- @javascript
Scenario: Question title can be changed from the question bank view
Given I am on the "Test quiz" "mod_quiz > question bank" page logged in as "teacher1"
- And I set the field "Select a category" to "Test questions"
+ And I set the field "Filter type" to "Category"
+ And I set the field "Category" to "Test questions"
+ And I press "Apply filters"
When I set the field "Edit question name" in the "First question" "table_row" to "Edited question"
Then I should not see "First question"
And I should see "Edited question"
- @javascript
Scenario: Teacher without permission can not change the title from question bank view
Given the following "role capability" exists:
| role | editingteacher |
| moodle/question:editall | prevent |
And I am on the "Test quiz" "mod_quiz > question bank" page logged in as "teacher1"
- When I set the field "Select a category" to "Test questions"
+ And I set the field "Filter type" to "Category"
+ And I set the field "Category" to "Test questions"
+ And I press "Apply filters"
And I should see "First question"
And "Edit question name" "field" should not exist
diff --git a/question/bank/viewquestiontext/amd/build/datafilter/filtertypes/showtext.min.js b/question/bank/viewquestiontext/amd/build/datafilter/filtertypes/showtext.min.js
new file mode 100644
index 00000000000..538127539f6
--- /dev/null
+++ b/question/bank/viewquestiontext/amd/build/datafilter/filtertypes/showtext.min.js
@@ -0,0 +1,11 @@
+define("qbank_viewquestiontext/datafilter/filtertypes/showtext",["exports","core/datafilter/filtertype"],(function(_exports,_filtertype){var obj;
+/**
+ * Filter managing question text display.
+ *
+ * @module qbank_viewquestiontext/datafilter/filtertypes/showtext
+ * @author 2022 Ghaly Marc-Alexandre
+ * @copyright 2022 Catalyst IT Australia Pty Ltd
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_filtertype=(obj=_filtertype)&&obj.__esModule?obj:{default:obj};class _default extends _filtertype.default{}return _exports.default=_default,_exports.default}));
+
+//# sourceMappingURL=showtext.min.js.map
\ No newline at end of file
diff --git a/question/bank/viewquestiontext/amd/build/datafilter/filtertypes/showtext.min.js.map b/question/bank/viewquestiontext/amd/build/datafilter/filtertypes/showtext.min.js.map
new file mode 100644
index 00000000000..719c279276c
--- /dev/null
+++ b/question/bank/viewquestiontext/amd/build/datafilter/filtertypes/showtext.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"showtext.min.js","sources":["../../../src/datafilter/filtertypes/showtext.js"],"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 .\n\n/**\n * Filter managing question text display.\n *\n * @module qbank_viewquestiontext/datafilter/filtertypes/showtext\n * @author 2022 Ghaly Marc-Alexandre \n * @copyright 2022 Catalyst IT Australia Pty Ltd\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport GenericFilter from 'core/datafilter/filtertype';\n\nexport default class extends GenericFilter {\n}\n"],"names":["GenericFilter"],"mappings":";;;;;;;;4KA0B6BA"}
\ No newline at end of file
diff --git a/question/bank/viewquestiontext/amd/src/datafilter/filtertypes/showtext.js b/question/bank/viewquestiontext/amd/src/datafilter/filtertypes/showtext.js
new file mode 100644
index 00000000000..5a88b46cabd
--- /dev/null
+++ b/question/bank/viewquestiontext/amd/src/datafilter/filtertypes/showtext.js
@@ -0,0 +1,28 @@
+// 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 .
+
+/**
+ * Filter managing question text display.
+ *
+ * @module qbank_viewquestiontext/datafilter/filtertypes/showtext
+ * @author 2022 Ghaly Marc-Alexandre
+ * @copyright 2022 Catalyst IT Australia Pty Ltd
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import GenericFilter from 'core/datafilter/filtertype';
+
+export default class extends GenericFilter {
+}
diff --git a/question/bank/viewquestiontext/classes/plugin_feature.php b/question/bank/viewquestiontext/classes/plugin_feature.php
index e102619dc62..2b7ac9c87f2 100644
--- a/question/bank/viewquestiontext/classes/plugin_feature.php
+++ b/question/bank/viewquestiontext/classes/plugin_feature.php
@@ -17,6 +17,7 @@
namespace qbank_viewquestiontext;
use core_question\local\bank\plugin_features_base;
+use core_question\local\bank\view;
/**
* Class columns is the entrypoint for the columns.
@@ -33,4 +34,10 @@ class plugin_feature extends plugin_features_base {
new question_text_row($qbank)
];
}
+
+ public function get_question_filters(view $qbank = null): array {
+ return [
+ new questiontext_condition($qbank),
+ ];
+ }
}
diff --git a/question/bank/viewquestiontext/classes/question_text_row.php b/question/bank/viewquestiontext/classes/question_text_row.php
index dfb3d3137c6..98dbc9e2ee4 100644
--- a/question/bank/viewquestiontext/classes/question_text_row.php
+++ b/question/bank/viewquestiontext/classes/question_text_row.php
@@ -54,20 +54,26 @@ class question_text_row extends row_base {
}
protected function display_content($question, $rowclasses): void {
- if ($this->plain) {
- $text = question_utils::to_plain_text($question->questiontext,
- $question->questiontextformat, ['noclean' => true, 'para' => false, 'filter' => false]);
- } else {
- $text = question_rewrite_question_preview_urls($question->questiontext, $question->id,
- $question->contextid, 'question', 'questiontext', $question->id,
- $question->contextid, 'core_question');
- $text = format_text($text, $question->questiontextformat,
- $this->formatoptions);
+ // Access 'showtext' filter from pagevars.
+ $display = $this->qbank->get_pagevars('filter')['showtext'] ?? null;
+ if ($display) {
+ $showtext = (int)$display['values'][0];
+ $text = '';
+ if ($showtext === questiontext_condition::PLAIN) {
+ $text = question_utils::to_plain_text($question->questiontext,
+ $question->questiontextformat, ['noclean' => true, 'para' => false, 'filter' => false]);
+ } else if ($showtext === questiontext_condition::FULL) {
+ $text = question_rewrite_question_preview_urls($question->questiontext, $question->id,
+ $question->contextid, 'question', 'questiontext', $question->id,
+ $question->contextid, 'core_question');
+ $text = format_text($text, $question->questiontextformat,
+ $this->formatoptions);
+ }
+ if ($text == '') {
+ $text = ' ';
+ }
+ echo $text;
}
- if ($text == '') {
- $text = ' ';
- }
- echo $text;
}
public function get_required_fields(): array {
@@ -75,14 +81,10 @@ class question_text_row extends row_base {
}
public function has_preference(): bool {
- return true;
+ return false;
}
public function get_preference_key(): string {
return 'qbshowtext';
}
-
- public function get_preference(): bool {
- return question_get_display_preference($this->get_preference_key(), 0, PARAM_BOOL, new \moodle_url(''));
- }
}
diff --git a/question/bank/viewquestiontext/classes/questiontext_condition.php b/question/bank/viewquestiontext/classes/questiontext_condition.php
new file mode 100644
index 00000000000..6d1d0df111e
--- /dev/null
+++ b/question/bank/viewquestiontext/classes/questiontext_condition.php
@@ -0,0 +1,108 @@
+.
+
+namespace qbank_viewquestiontext;
+
+use core\output\datafilter;
+use core_question\local\bank\condition;
+use core_question\local\bank\view;
+
+/**
+ * This class controls from which category questions are listed.
+ *
+ * @package qbank_viewquestiontext
+ * @copyright 2022 Catalyst IT Australia Pty Ltd
+ * @author Ghaly Marc-Alexandre
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class questiontext_condition extends condition {
+
+ /**
+ * @var int Question text is off.
+ */
+ const OFF = 0;
+
+ /**
+ * @var int Question text is displayed in plain text mode.
+ */
+ const PLAIN = 1;
+
+ /**
+ * @var int Question text is displayed fully rendered.
+ */
+ const FULL = 2;
+
+ /**
+ * @var ?int $showtext Initial value of the filter, for determining the pre-selected option.
+ */
+ protected ?int $showtext = null;
+
+ public function __construct(view $qbank = null) {
+ if (is_null($qbank)) {
+ return;
+ }
+ $filter = $qbank->get_pagevars('filter');
+ if (isset($filter['showtext'])) {
+ $this->showtext = (int)$filter['showtext']['values'][0];
+ }
+ }
+
+ public function allow_custom() {
+ return false;
+ }
+
+ public function allow_multiple() {
+ return false;
+ }
+
+ public static function get_condition_key() {
+ return 'showtext';
+ }
+
+ public function get_title() {
+ return get_string('showquestiontext', 'core_question');
+ }
+
+ public function get_join_list(): array {
+ return [
+ datafilter::JOINTYPE_ANY,
+ ];
+ }
+
+ public function get_filter_class() {
+ return 'qbank_viewquestiontext/datafilter/filtertypes/showtext';
+ }
+
+ public function get_initial_values() {
+ return [
+ [
+ 'value' => self::OFF,
+ 'title' => get_string('showquestiontext_off', 'question'),
+ 'selected' => $this->showtext === self::OFF,
+ ],
+ [
+ 'value' => self::PLAIN,
+ 'title' => get_string('showquestiontext_plain', 'question'),
+ 'selected' => $this->showtext === self::PLAIN,
+ ],
+ [
+ 'value' => self::FULL,
+ 'title' => get_string('showquestiontext_full', 'question'),
+ 'selected' => $this->showtext === self::FULL,
+ ]
+ ];
+ }
+}
diff --git a/question/bank/viewquestiontext/tests/behat/view_question_name_column.feature b/question/bank/viewquestiontext/tests/behat/view_question_text_column.feature
similarity index 58%
rename from question/bank/viewquestiontext/tests/behat/view_question_name_column.feature
rename to question/bank/viewquestiontext/tests/behat/view_question_text_column.feature
index 04198652a57..a2829f2adec 100644
--- a/question/bank/viewquestiontext/tests/behat/view_question_name_column.feature
+++ b/question/bank/viewquestiontext/tests/behat/view_question_text_column.feature
@@ -19,23 +19,39 @@ Feature: Use the qbank plugin manager page for viewquestiontext
@javascript
Scenario: Display of plain question text can be turned on and off
When I am on the "Test quiz" "mod_quiz > question bank" page logged in as admin
- And I set the field "Show question text in the question list?" to "Yes, text only"
+ And I apply question bank filter "Show question text in the question list?" with value "text only"
Then I should see "Answer the first question"
And ".totestforhtml" "css_element" should not exist in the "Answer the first question" "table_row"
- And I set the field "Show question text in the question list?" to "No"
+ And I apply question bank filter "Show question text in the question list?" with value "No"
And I should not see "Answer the first question"
@javascript
Scenario: Display of full question text can be turned on and off
When I am on the "Test quiz" "mod_quiz > question bank" page logged in as admin
- And I set the field "Show question text in the question list?" to "Yes, with images, media, etc."
+ And I apply question bank filter "Show question text in the question list?" with value "with images"
Then I should see "Answer the first question"
And ".totestforhtml" "css_element" should exist in the "Answer the first question" "table_row"
- And I set the field "Show question text in the question list?" to "No"
+ And I apply question bank filter "Show question text in the question list?" with value "No"
And I should not see "Answer the first question"
+ @javascript
Scenario: Option does not show if the plugin is disabled
Given the following config values are set as admin:
| disabled | 1 | qbank_viewquestiontext |
When I am on the "Test quiz" "mod_quiz > question bank" page logged in as admin
- Then I should not see "Show question text in the question list?"
+ Then I should not see "Show question text in the question list"
+
+ @javascript
+ Scenario: Enable/disable viewquestiontext column from the base view
+ Given I log in as "admin"
+ And I navigate to "Plugins > Question bank plugins > Manage question bank plugins" in site administration
+ And I should see "View question text"
+ When I click on "Disable" "link" in the "View question text" "table_row"
+ And I am on the "Test quiz" "mod_quiz > question bank" page
+ And I should not see "Show question text in the question list"
+ Then "#categoryquestions .questiontext" "css_element" should not be visible
+ And I navigate to "Plugins > Question bank plugins > Manage question bank plugins" in site administration
+ And I click on "Enable" "link" in the "View question text" "table_row"
+ And I am on the "Test quiz" "mod_quiz > question bank" page
+ When I apply question bank filter "Show question text in the question list?" with value "text only"
+ And I should see "Answer the first question"
diff --git a/question/classes/bank/search/category_condition.php b/question/classes/bank/search/category_condition.php
deleted file mode 100644
index 5af6f5074f1..00000000000
--- a/question/classes/bank/search/category_condition.php
+++ /dev/null
@@ -1,205 +0,0 @@
-.
-
-
-/**
- * A search class to control from which category questions are listed.
- *
- * @package core_question
- * @copyright 2013 Ray Morris
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-namespace core_question\bank\search;
-
-use qbank_managecategories\helper;
-
-/**
- * This class controls from which category questions are listed.
- *
- * @copyright 2013 Ray Morris
- * @author 2021 Safat Shahin
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class category_condition extends condition {
- /** @var \stdClass The course record. */
- protected $course;
-
- /** @var \stdClass The category record. */
- protected $category;
-
- /** @var array of contexts. */
- protected $contexts;
-
- /** @var bool Whether to include questions from sub-categories. */
- protected $recurse;
-
- /** @var string SQL fragment to add to the where clause. */
- protected $where;
-
- /** @var array query param used in where. */
- protected $params;
-
- /** @var string categoryID,contextID as used with question_bank_view->display(). */
- protected $cat;
-
- /** @var int The maximum displayed length of the category info. */
- protected $maxinfolength;
-
- /** @var \moodle_url The URL the form is submitted to. */
- protected $baseurl;
-
- /**
- * Constructor
- * @param string $cat categoryID,contextID as used with question_bank_view->display()
- * @param bool $recurse Whether to include questions from sub-categories
- * @param array $contexts Context objects as used by question_category_options()
- * @param \moodle_url $baseurl The URL the form is submitted to
- * @param \stdClass $course Course record
- * @param integer $maxinfolength The maximum displayed length of the category info.
- */
- public function __construct($cat, $recurse, $contexts, $baseurl, $course, $maxinfolength = null) {
- $this->cat = $cat;
- $this->recurse = $recurse;
- $this->contexts = $contexts;
- $this->baseurl = $baseurl;
- $this->course = $course;
- $this->init();
- $this->maxinfolength = $maxinfolength;
- }
-
- /**
- * Initialize the object so it will be ready to return where() and params()
- */
- private function init() {
- global $DB;
- if (!$this->category = $this->get_current_category($this->cat)) {
- return;
- }
- if ($this->recurse) {
- $categoryids = question_categorylist($this->category->id);
- } else {
- $categoryids = [$this->category->id];
- }
- list($catidtest, $this->params) = $DB->get_in_or_equal($categoryids, SQL_PARAMS_NAMED, 'cat');
- $this->where = 'qbe.questioncategoryid ' . $catidtest;
- }
-
- /**
- * SQL fragment to add to the where clause.
- *
- * @return string
- */
- public function where() {
- return $this->where;
- }
-
- /**
- * Return parameters to be bound to the above WHERE clause fragment.
- * @return array parameter name => value.
- */
- public function params() {
- return $this->params;
- }
-
- /**
- * Called by question_bank_view to display the GUI for selecting a category
- */
- public function display_options() {
- global $PAGE;
- $displaydata = [];
- $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'));
- $displaydata['categorydesc'] = '';
- if ($this->category) {
- $displaydata['categorydesc'] = $this->print_category_info($this->category);
- }
- return $PAGE->get_renderer('core_question', 'bank')->render_category_condition($displaydata);
- }
-
- /**
- * Displays the recursion checkbox GUI.
- * question_bank_view places this within the section that is hidden by default
- */
- public function display_options_adv() {
- global $PAGE;
- $displaydata = [];
- if ($this->recurse) {
- $displaydata['checked'] = 'checked';
- }
- return $PAGE->get_renderer('core_question', 'bank')->render_category_condition_advanced($displaydata);
- }
-
- /**
- * Display the drop down to select the category.
- *
- * @param array $contexts of contexts that can be accessed from here.
- * @param \moodle_url $pageurl the URL of this page.
- * @param string $current 'categoryID,contextID'.
- * @deprecated since Moodle 4.0
- */
- protected function display_category_form($contexts, $pageurl, $current) {
- debugging('Function display_category_form() is deprecated,
- please use the core_question renderer instead.', DEBUG_DEVELOPER);
- echo \html_writer::start_div('choosecategory');
- $catmenu = question_category_options($contexts, true, 0, true, -1, false);
- echo \html_writer::label(get_string('selectacategory', 'question'), 'id_selectacategory', true, ["class" => "mr-1"]);
- echo \html_writer::select($catmenu, 'category', $current, [],
- array('class' => 'searchoptions custom-select', 'id' => 'id_selectacategory'));
- echo \html_writer::end_div() . "\n";
- }
-
- /**
- * Look up the category record based on cateogry ID and context
- * @param string $categoryandcontext categoryID,contextID as used with question_bank_view->display()
- * @return \stdClass The category record
- */
- protected function get_current_category($categoryandcontext) {
- global $DB, $OUTPUT;
- list($categoryid, $contextid) = explode(',', $categoryandcontext);
- if (!$categoryid) {
- $this->print_choose_category_message($categoryandcontext);
- return false;
- }
-
- if (!$category = $DB->get_record('question_categories', ['id' => $categoryid, 'contextid' => $contextid])) {
- echo $OUTPUT->box_start('generalbox questionbank');
- echo $OUTPUT->notification('Category not found!');
- echo $OUTPUT->box_end();
- return false;
- }
-
- return $category;
- }
-
- /**
- * Print the category description
- * @param \stdClass $category the category information form the database.
- */
- protected function print_category_info($category): string {
- $formatoptions = new \stdClass();
- $formatoptions->noclean = true;
- $formatoptions->overflowdiv = true;
- if (isset($this->maxinfolength)) {
- return shorten_text(format_text($category->info, $category->infoformat, $formatoptions, $this->course->id),
- $this->maxinfolength);
- } else {
- return format_text($category->info, $category->infoformat, $formatoptions, $this->course->id);
- }
- }
-}
diff --git a/question/classes/bank/search/condition.php b/question/classes/bank/search/condition.php
deleted file mode 100644
index 5e6b8475bde..00000000000
--- a/question/classes/bank/search/condition.php
+++ /dev/null
@@ -1,69 +0,0 @@
-.
-
-
-/**
- * Defines an abstract class for filtering/searching the question bank.
- *
- * @package core_question
- * @copyright 2013 Ray Morris
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-namespace core_question\bank\search;
-
-/**
- * An abstract class for filtering/searching questions.
- *
- * See also {@see question_bank_view::init_search_conditions()}.
- * @copyright 2013 Ray Morris
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-abstract class condition {
- /**
- * Return an SQL fragment to be ANDed into the WHERE clause to filter which questions are shown.
- * @return string SQL fragment. Must use named parameters.
- */
- abstract public function where();
-
- /**
- * Return parameters to be bound to the above WHERE clause fragment.
- * @return array parameter name => value.
- */
- public function params() {
- return [];
- }
-
- /**
- * Display GUI for selecting criteria for this condition. Displayed when Show More is open.
- *
- * Compare display_options(), which displays always, whether Show More is open or not.
- * @return bool|string HTML form fragment
- */
- public function display_options_adv() {
- return false;
- }
-
- /**
- * Display GUI for selecting criteria for this condition. Displayed always, whether Show More is open or not.
- *
- * Compare display_options_adv(), which displays when Show More is open.
- * @return bool|string HTML form fragment
- */
- public function display_options() {
- return false;
- }
-}
diff --git a/question/classes/bank/search/hidden_condition.php b/question/classes/bank/search/hidden_condition.php
deleted file mode 100644
index a05fc35ebbb..00000000000
--- a/question/classes/bank/search/hidden_condition.php
+++ /dev/null
@@ -1,76 +0,0 @@
-.
-
-
-/**
- * A search class to control whether hidden / deleted questions are hidden in the list.
- *
- * @package core_question
- * @copyright 2013 Ray Morris
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-namespace core_question\bank\search;
-
-use core_question\local\bank\question_version_status;
-
-/**
- * This class controls whether hidden / deleted questions are hidden in the list.
- *
- * @copyright 2013 Ray Morris
- * @author 2021 Safat Shahin
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class hidden_condition extends condition {
- /** @var bool Whether to include old "deleted" questions. */
- protected $hide;
-
- /** @var string SQL fragment to add to the where clause. */
- protected $where;
-
- /**
- * Constructor.
- * @param bool $hide whether to include old "deleted" questions.
- */
- public function __construct($hide = true) {
- $this->hide = $hide;
- if ($hide) {
- $this->where = "qv.status = '" . question_version_status::QUESTION_STATUS_READY . "' " .
- " OR qv.status = '" . question_version_status::QUESTION_STATUS_DRAFT . "' ";
- }
- }
-
- /**
- * SQL fragment to add to the where clause.
- *
- * @return string
- */
- public function where() {
- return $this->where;
- }
-
- /**
- * Print HTML to display the "Also show old questions" checkbox
- */
- public function display_options_adv() {
- global $PAGE;
- $displaydata = [];
- if (!$this->hide) {
- $displaydata['checked'] = 'checked';
- }
- return $PAGE->get_renderer('core_question', 'bank')->render_hidden_condition_advanced($displaydata);
- }
-}
diff --git a/question/classes/bank/search/tag_condition.php b/question/classes/bank/search/tag_condition.php
deleted file mode 100644
index 89453b894a6..00000000000
--- a/question/classes/bank/search/tag_condition.php
+++ /dev/null
@@ -1,131 +0,0 @@
-.
-
-/**
- * A condition for adding filtering by tag to the question bank.
- *
- * @package core_question
- * @copyright 2018 Ryan Wyllie
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-namespace core_question\bank\search;
-
-/**
- * Question bank search class to allow searching/filtering by tags on a question.
- *
- * @copyright 2018 Ryan Wyllie
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class tag_condition extends condition {
-
- /** @var string SQL fragment to add to the where clause. */
- protected $where;
-
- /** @var array Named SQL params to be used with the SQL WHERE snippet. */
- protected $params;
-
- /** @var array List of contexts to show tags from. */
- protected $contexts;
-
- /** @var array List of IDs for tags that have been selected in the form. */
- protected $selectedtagids;
-
- /**
- * Constructor.
- *
- * @param array $contexts List of contexts to show tags from
- * @param int[] $selectedtagids List of IDs for tags to filter by.
- * @throws \coding_exception
- * @throws \dml_exception
- */
- public function __construct(array $contexts, array $selectedtagids = []) {
- global $DB;
-
- $this->contexts = $contexts;
-
- // If some tags have been selected then we need to filter
- // the question list by the selected tags.
- if ($selectedtagids) {
- // We treat each additional tag as an AND condition rather than
- // an OR condition.
- //
- // For example, if the user filters by the tags "foo" and "bar" then
- // we reduce the question list to questions that are tagged with both
- // "foo" AND "bar". Any question that does not have ALL of the specified
- // tags will be omitted.
- list($tagsql, $tagparams) = $DB->get_in_or_equal($selectedtagids, SQL_PARAMS_NAMED);
- $tagparams['tagcount'] = count($selectedtagids);
- $tagparams['questionitemtype'] = 'question';
- $tagparams['questioncomponent'] = 'core_question';
- $this->selectedtagids = $selectedtagids;
- $this->params = $tagparams;
- $this->where = "q.id IN (SELECT ti.itemid
- FROM {tag_instance} ti
- WHERE ti.itemtype = :questionitemtype
- AND ti.component = :questioncomponent
- AND ti.tagid {$tagsql}
- GROUP BY ti.itemid
- HAVING COUNT(itemid) = :tagcount)";
-
- } else {
- $this->selectedtagids = [];
- $this->params = [];
- $this->where = '';
- }
- }
-
- /**
- * Get the SQL WHERE snippet to be used in the SQL to retrieve the
- * list of questions. This SQL snippet will add the logic for the
- * tag condition.
- *
- * @return string
- */
- public function where() {
- return $this->where;
- }
-
- /**
- * Named SQL params to be used with the SQL WHERE snippet.
- *
- * @return array
- */
- public function params() {
- return $this->params;
- }
-
- /**
- * Print HTML to display the list of tags to filter by.
- */
- public function display_options() {
- global $OUTPUT;
-
- $tags = \core_tag_tag::get_tags_by_area_in_contexts('core_question', 'question', $this->contexts);
- $tagoptions = array_map(function($tag) {
- return [
- 'id' => $tag->id,
- 'name' => $tag->name,
- 'selected' => in_array($tag->id, $this->selectedtagids)
- ];
- }, array_values($tags));
- $context = [
- 'tagoptions' => $tagoptions
- ];
-
- return $OUTPUT->render_from_template('core_question/tag_condition', $context);
- }
-}
diff --git a/question/classes/external.php b/question/classes/external.php
index e3244342886..782345c612b 100644
--- a/question/classes/external.php
+++ b/question/classes/external.php
@@ -314,8 +314,22 @@ class core_question_external extends external_api {
$loader = new \core_question\local\bank\random_question_loader(new qubaid_list([]));
// Only load the properties we require from the DB.
$properties = \core_question\external\question_summary_exporter::get_mandatory_properties();
- $questions = $loader->get_questions($categoryid, $includesubcategories, $tagids, $limit, $offset, $properties);
- $totalcount = $loader->count_questions($categoryid, $includesubcategories, $tagids);
+
+ // Transform to filters.
+ $filters = [
+ 'category' => [
+ 'jointype' => \qbank_managecategories\category_condition::JOINTYPE_DEFAULT,
+ 'values' => [$categoryid],
+ 'filteroptions' => ['includesubcategories' => $includesubcategories],
+ ],
+ 'qtagids' => [
+ 'jointype' => \qbank_tagquestion\tag_condition::JOINTYPE_DEFAULT,
+ 'values' => $tagids,
+ ],
+ ];
+
+ $questions = $loader->get_filtered_questions($filters, $limit, $offset, $properties);
+ $totalcount = $loader->count_filtered_questions($filters);
$renderer = $PAGE->get_renderer('core');
$formattedquestions = array_map(function($question) use ($context, $renderer) {
diff --git a/question/classes/local/bank/column_base.php b/question/classes/local/bank/column_base.php
index 66832ed337e..2875d918762 100644
--- a/question/classes/local/bank/column_base.php
+++ b/question/classes/local/bank/column_base.php
@@ -87,7 +87,7 @@ abstract class column_base extends view_component {
$data['sortable'] = true;
$data['extraclasses'] = $this->get_classes();
$sortable = $this->is_sortable();
- $name = get_class($this);
+ $name = str_replace('\\', '__', get_class($this));
$title = $this->get_title();
$tip = $this->get_title_tip();
$links = [];
@@ -144,19 +144,20 @@ abstract class column_base extends view_component {
/**
* Get a link that changes the sort order, and indicates the current sort state.
- * @param string $sort the column to sort on.
+ *
+ * @param string $sortname the column to sort on.
* @param string $title the link text.
* @param string $tip the link tool-tip text. If empty, defaults to title.
* @param bool $defaultreverse whether the default sort order for this column is descending, rather than ascending.
* @return string
*/
- protected function make_sort_link($sort, $title, $tip, $defaultreverse = false): string {
+ protected function make_sort_link($sortname, $title, $tip, $defaultreverse = false): string {
global $PAGE;
$sortdata = [];
- $currentsort = $this->qbank->get_primary_sort_order($sort);
+ $currentsort = $this->qbank->get_primary_sort_order($sortname);
$newsortreverse = $defaultreverse;
if ($currentsort) {
- $newsortreverse = $currentsort > 0;
+ $newsortreverse = $currentsort == SORT_ASC;
}
if (!$tip) {
$tip = $title;
@@ -169,12 +170,14 @@ abstract class column_base extends view_component {
$link = $title;
if ($currentsort) {
- $link .= $this->get_sort_icon($currentsort < 0);
+ $link .= $this->get_sort_icon($currentsort == SORT_DESC);
}
- $sortdata['sorturl'] = $this->qbank->new_sort_url($sort, $newsortreverse);
+ $sortdata['sorturl'] = $this->qbank->new_sort_url($sortname, $newsortreverse);
+ $sortdata['sortname'] = $sortname;
$sortdata['sortcontent'] = $link;
$sortdata['sorttip'] = $tip;
+ $sortdata['sortorder'] = $newsortreverse ? SORT_DESC : SORT_ASC;
$renderer = $PAGE->get_renderer('core_question', 'bank');
return $renderer->render_column_sort($sortdata);
diff --git a/question/classes/local/bank/condition.php b/question/classes/local/bank/condition.php
new file mode 100644
index 00000000000..f1715adb60b
--- /dev/null
+++ b/question/classes/local/bank/condition.php
@@ -0,0 +1,222 @@
+.
+
+namespace core_question\local\bank;
+
+use core\output\datafilter;
+
+/**
+ * An abstract class for filtering/searching questions.
+ *
+ * @package core_question
+ * @copyright 2013 Ray Morris
+ * @author Safat Shahin
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class condition {
+
+ /** @var int The default filter type */
+ const JOINTYPE_DEFAULT = datafilter::JOINTYPE_ANY;
+
+ /** @var ?array Filter properties for this condition. */
+ public ?array $filter;
+
+ /** @var string SQL fragment to add to the where clause. */
+ protected string $where = '';
+
+ /** @var array query param used in where. */
+ protected array $params = [];
+
+ /**
+ * Return title of the condition
+ *
+ * @return string title of the condition
+ */
+ abstract public function get_title();
+
+ /**
+ * Return filter class associated with this condition
+ *
+ * @return string filter class
+ */
+ abstract public function get_filter_class();
+
+ /**
+ * Extract the required filter from the provided question bank view.
+ *
+ * This will look for the filter matching {@see get_condition_key()}
+ *
+ * @param view|null $qbank
+ */
+ public function __construct(view $qbank = null) {
+ if (is_null($qbank)) {
+ return;
+ }
+ $this->filter = static::get_filter_from_list($qbank->get_pagevars('filter'));
+ // Build where and params.
+ [$this->where, $this->params] = $this->filter ? static::build_query_from_filter($this->filter) : ['', []];
+ }
+
+ /**
+ * Whether customisation is allowed.
+ *
+ * @return bool
+ */
+ public function allow_custom() {
+ return true;
+ }
+
+ /**
+ * Whether multiple values are allowed .
+ *
+ * @return bool
+ */
+ public function allow_multiple() {
+ return true;
+ }
+
+ /**
+ * Initial values of the condition
+ *
+ * @return array
+ */
+ public function get_initial_values() {
+ return [];
+ }
+
+ /**
+ * Extra data specific to this condition.
+ *
+ * @return \stdClass
+ */
+ public function get_filteroptions(): \stdClass {
+ return (object)[];
+ }
+
+ /**
+ * Whether empty value is allowed
+ *
+ * @return bool
+ */
+ public function allow_empty() {
+ return true;
+ }
+
+ /**
+ * Whether this filter is required - if so it cannot be removed from the list of filters.
+ *
+ * @return bool
+ */
+ public function is_required(): bool {
+ return false;
+ }
+
+ /**
+ * Return this condition class
+ *
+ * @return string
+ */
+ public function get_condition_class() {
+ return get_class($this);
+ }
+
+ /**
+ * Each condition will need a unique key to be identified and sequenced by the api.
+ * Use a unique string for the condition identifier, use string directly, dont need to use language pack.
+ * Using language pack might break the filter object for multilingual support.
+ *
+ * @return string
+ */
+ public static function get_condition_key() {
+ return '';
+ }
+
+ /**
+ * Return an SQL fragment to be ANDed into the WHERE clause to filter which questions are shown.
+ *
+ * @return string SQL fragment. Must use named parameters.
+ */
+ public function where() {
+ return $this->where;
+ }
+
+ /**
+ * Return parameters to be bound to the above WHERE clause fragment.
+ * @return array parameter name => value.
+ */
+ public function params() {
+ return $this->params;
+ }
+
+ /**
+ * Display GUI for selecting criteria for this condition. Displayed when Show More is open.
+ *
+ * Compare display_options(), which displays always, whether Show More is open or not.
+ * @return bool|string HTML form fragment
+ * @deprecated since Moodle 4.0 MDL-72321 - please do not use this function any more.
+ * @todo Final deprecation on Moodle 4.1 MDL-72572
+ */
+ public function display_options_adv() {
+ debugging('Function display_options_adv() is deprecated, please use filtering objects', DEBUG_DEVELOPER);
+ return false;
+ }
+
+ /**
+ * Display GUI for selecting criteria for this condition. Displayed always, whether Show More is open or not.
+ *
+ * Compare display_options_adv(), which displays when Show More is open.
+ * @return bool|string HTML form fragment
+ * @deprecated since Moodle 4.0 MDL-72321 - please do not use this function any more.
+ * @todo Final deprecation on Moodle 4.1 MDL-72572
+ */
+ public function display_options() {
+ debugging('Function display_options() is deprecated, please use filtering objects', DEBUG_DEVELOPER);
+ return false;
+ }
+
+ /**
+ * Get the list of available joins for the filter.
+ *
+ * @return array
+ */
+ public function get_join_list(): array {
+ return [
+ datafilter::JOINTYPE_NONE,
+ datafilter::JOINTYPE_ANY,
+ datafilter::JOINTYPE_ALL,
+ ];
+ }
+
+ /**
+ * Given an array of filters, pick the entry that matches the condition key and return it.
+ *
+ * @param array $filters Array of filters, keyed by condition.
+ * @return ?array The filter that matches this condition
+ */
+ public static function get_filter_from_list(array $filters): ?array {
+ return $filters[static::get_condition_key()] ?? null;
+ }
+
+ /**
+ * Build query from filter value
+ *
+ * @param array $filter filter properties
+ * @return array where sql and params
+ */
+ public static function build_query_from_filter(array $filter): array {
+ return ['', []];
+ }
+}
diff --git a/question/classes/local/bank/filter_condition_manager.php b/question/classes/local/bank/filter_condition_manager.php
new file mode 100644
index 00000000000..abf1cf2058f
--- /dev/null
+++ b/question/classes/local/bank/filter_condition_manager.php
@@ -0,0 +1,113 @@
+.
+
+/**
+ * Functions for managing and manipulating question filter conditions
+ *
+ * @package core_question
+ * @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
+ * @author Mark Johnson
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_question\local\bank;
+
+/**
+ * Static methods for parsing and formatting data related to filter conditions.
+ */
+class filter_condition_manager {
+
+ /**
+ * Extract parameters from args list.
+ *
+ * @param array $args
+ * @return array the param and extra param
+ */
+ public static function extract_parameters_from_fragment_args(array $args): array {
+ global $DB;
+ // Decode query string.
+ $filtercondition = json_decode($args['filtercondition'], true);
+ $categories = $DB->get_records('question_categories', ['id' => clean_param($filtercondition['cat'], PARAM_INT)]);
+ $categories = \qbank_managecategories\helper::question_add_context_in_key($categories);
+ $category = array_pop($categories);
+ $filtercondition['cat'] = $category->id;
+ $extraparams = json_decode($args['extraparams'], true);
+
+ return [$filtercondition, $extraparams];
+ }
+
+ /**
+ * List of condition classes
+ *
+ * @return condition[] condition classes: [condition_key] = class
+ */
+ public static function get_condition_classes(): array {
+ $classes = [];
+ $plugins = \core_component::get_plugin_list_with_class('qbank', 'plugin_feature', 'plugin_feature.php');
+ foreach ($plugins as $componentname => $plugin) {
+ if (\core\plugininfo\qbank::is_plugin_enabled($componentname)) {
+ $pluginentrypointobject = new $plugin();
+ $conditions = $pluginentrypointobject->get_question_filters();
+ foreach ($conditions as $condition) {
+ $classes[$condition->get_condition_key()] = $condition->get_condition_class();
+ }
+ }
+ }
+ return $classes;
+ }
+
+ /**
+ * Given a JSON-encoded "filter" URL param, create or replace the category filter with the provided category.
+ *
+ * @param string $filterparam The json-encoded filter param from the URL, containing the list of filters.
+ * @param int $newcategoryid The new ID to set for the "category" filter condition's value.
+ * @return string JSON-encoded filter param with the new category.
+ */
+ public static function update_filter_param_to_category(string $filterparam, int $newcategoryid): string {
+ $returnfilters = json_decode($filterparam, true);
+ if (!$returnfilters) {
+ $returnfilters = [
+ 'category' => [
+ 'name' => 'category',
+ 'jointype' => \core_question\local\bank\condition::JOINTYPE_DEFAULT,
+ ]
+ ];
+ }
+ $returnfilters['category']['values'] = [$newcategoryid];
+ return json_encode($returnfilters);
+ }
+
+ /**
+ * Unpack filteroptions passed in a request's filter param if required.
+ *
+ * Filteroptions are passed via AJAX as an array of {name:, value:} pairs for compatibility with external functions.
+ *
+ * @param array $filters List of filters, each optionally including an array of filteroptions.
+ * @return array The input array, with filteroptions unpacked from [{name:, value:}, ...] to [name => value, ...].
+ */
+ public static function unpack_filteroptions_param(array $filters): array {
+ foreach ($filters as $name => $filter) {
+ if (!empty($filter['filteroptions']) && isset(reset($filter['filteroptions'])['name'])) {
+ $unpacked = [];
+ foreach ($filter['filteroptions'] as $filteroption) {
+ $unpacked[$filteroption['name']] = $filteroption['value'];
+ }
+ $filters[$name]['filteroptions'] = $unpacked;
+ }
+ }
+ return $filters;
+ }
+}
diff --git a/question/classes/local/bank/plugin_features_base.php b/question/classes/local/bank/plugin_features_base.php
index de42d3cf8eb..5787da8f6a5 100644
--- a/question/classes/local/bank/plugin_features_base.php
+++ b/question/classes/local/bank/plugin_features_base.php
@@ -75,4 +75,13 @@ class plugin_features_base {
return [];
}
+ /**
+ * Return search conditions for the plugin.
+ *
+ * @param view|null $qbank
+ * @return condition[]
+ */
+ public function get_question_filters(view $qbank = null): array {
+ return [];
+ }
}
diff --git a/question/classes/local/bank/random_question_loader.php b/question/classes/local/bank/random_question_loader.php
index 5497b624133..7daa6993d40 100644
--- a/question/classes/local/bank/random_question_loader.php
+++ b/question/classes/local/bank/random_question_loader.php
@@ -76,6 +76,29 @@ class random_question_loader {
}
}
+ /**
+ * Pick a random question based on filter conditions
+ *
+ * @param array $filters filter array
+ * @return int|null
+ */
+ public function get_next_filtered_question_id(array $filters): ?int {
+ $this->ensure_filtered_questions_loaded($filters);
+
+ $key = $this->get_filtered_questions_key($filters);
+ if (empty($this->availablequestionscache[$key])) {
+ return null;
+ }
+
+ reset($this->availablequestionscache[$key]);
+ $lowestcount = key($this->availablequestionscache[$key]);
+ reset($this->availablequestionscache[$key][$lowestcount]);
+ $questionid = key($this->availablequestionscache[$key][$lowestcount]);
+ $this->use_question($questionid);
+ return $questionid;
+ }
+
+
/**
* Pick a question at random from the given category, from among those with the fewest uses.
* If an array of tag ids are specified, then only the questions that are tagged with ALL those tags will be selected.
@@ -89,8 +112,15 @@ class random_question_loader {
* @param array $tagids An array of tag ids. A question has to be tagged with all the provided tagids (if any)
* in order to be eligible for being picked.
* @return int|null the id of the question picked, or null if there aren't any.
+ * @deprecated since Moodle 4.3
+ * @todo Final deprecation on Moodle 4.7 MDL-78091
*/
public function get_next_question_id($categoryid, $includesubcategories, $tagids = []): ?int {
+ debugging(
+ 'Function get_next_question_id() is deprecated, please use get_next_filtered_question_id() instead.',
+ DEBUG_DEVELOPER
+ );
+
$this->ensure_questions_for_category_loaded($categoryid, $includesubcategories, $tagids);
$categorykey = $this->get_category_key($categoryid, $includesubcategories, $tagids);
@@ -106,6 +136,17 @@ class random_question_loader {
return $questionid;
}
+ /**
+ * Key for filtered questions.
+ * This function replace get_category_key
+ *
+ * @param array $filters filter array
+ * @return String
+ */
+ protected function get_filtered_questions_key(array $filters): String {
+ return sha1(json_encode($filters));
+ }
+
/**
* Get the key into {@see $availablequestionscache} for this combination of options.
*
@@ -114,8 +155,15 @@ class random_question_loader {
* that category, or that category and subcategories.
* @param array $tagids an array of tag ids.
* @return string the cache key.
+ *
+ * @deprecated since Moodle 4.3
+ * @todo Final deprecation on Moodle 4.7 MDL-78091
*/
protected function get_category_key($categoryid, $includesubcategories, $tagids = []): string {
+ debugging(
+ 'Function get_category_key() is deprecated, please get_fitlered_questions_key instead.',
+ DEBUG_DEVELOPER
+ );
if ($includesubcategories) {
$key = $categoryid . '|1';
} else {
@@ -129,6 +177,106 @@ class random_question_loader {
return $key;
}
+ /**
+ * Populate {@see $availablequestionscache} according to filter conditions.
+ *
+ * @param array $filters filter array
+ * @return void
+ */
+ protected function ensure_filtered_questions_loaded(array $filters) {
+ global $DB;
+
+ $key = $this->get_filtered_questions_key($filters);
+ if (isset($this->availablequestionscache[$key])) {
+ // Data is already in the cache, nothing to do.
+ return;
+ }
+
+ [$extraconditions, $extraparams] = $DB->get_in_or_equal($this->excludedqtypes,
+ SQL_PARAMS_NAMED, 'excludedqtype', false);
+
+ $previoussql = "SELECT COUNT(1)
+ FROM " . $this->qubaids->from_question_attempts('qa') . "
+ WHERE qa.questionid = q.id AND " . $this->qubaids->where();
+ $previousparams = $this->qubaids->from_where_params();
+
+ // Latest version.
+ $latestversionsql = "SELECT MAX(v.version)
+ FROM {question_versions} v
+ JOIN {question_bank_entries} be ON be.id = v.questionbankentryid
+ WHERE be.id = qbe.id";
+
+ $sql = "SELECT q.id, ($previoussql) AS previous_attempts
+ FROM {question} q
+ JOIN {question_versions} qv ON qv.questionid = q.id
+ JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
+ WHERE ";
+
+ $where = [
+ 'q.parent = :noparent',
+ 'qv.status = :ready',
+ "qv.version = ($latestversionsql)",
+ ];
+ $params = array_merge(
+ $previousparams,
+ ['noparent' => 0, 'ready' => question_version_status::QUESTION_STATUS_READY]);
+
+ // Get current enabled condition classes.
+ $conditionclasses = \core_question\local\bank\filter_condition_manager::get_condition_classes();
+ // Build filter conditions.
+ foreach ($conditionclasses as $conditionclass) {
+ $filter = $conditionclass::get_filter_from_list($filters);
+ if (is_null($filter)) {
+ continue;
+ }
+ [$filterwhere, $filterparams] = $conditionclass::build_query_from_filter($filter);
+ if (!empty($filterwhere)) {
+ $where[] = '(' . $filterwhere . ')';
+ }
+ if (!empty($filterparams)) {
+ $params = array_merge($params, $filterparams);
+ }
+ }
+
+ // Extra conditions.
+ if ($extraconditions) {
+ $where[] = 'q.qtype ' . $extraconditions;
+ $params = array_merge($params, $extraparams);
+ }
+
+ // Build query.
+ $sql .= implode(' AND ', $where);
+ $sql .= "ORDER BY previous_attempts";
+
+ $questionidsandcounts = $DB->get_records_sql_menu($sql, $params);
+
+ if (!$questionidsandcounts) {
+ // No questions in this category.
+ $this->availablequestionscache[$key] = [];
+ return;
+ }
+
+ // Put all the questions with each value of $prevusecount in separate arrays.
+ $idsbyusecount = [];
+ foreach ($questionidsandcounts as $questionid => $prevusecount) {
+ if (isset($this->recentlyusedquestions[$questionid])) {
+ // Recently used questions are never returned.
+ continue;
+ }
+ $idsbyusecount[$prevusecount][] = $questionid;
+ }
+
+ // Now put that data into our cache. For each count, we need to shuffle
+ // questionids, and make those the keys of an array.
+ $this->availablequestionscache[$key] = [];
+ foreach ($idsbyusecount as $prevusecount => $questionids) {
+ shuffle($questionids);
+ $this->availablequestionscache[$key][$prevusecount] = array_combine(
+ $questionids, array_fill(0, count($questionids), 1));
+ }
+ ksort($this->availablequestionscache[$key]);
+ }
+
/**
* Populate {@see $availablequestionscache} for this combination of options.
*
@@ -137,8 +285,16 @@ class random_question_loader {
* that category, or that category and subcategories.
* @param array $tagids An array of tag ids. If an array is provided, then
* only the questions that are tagged with ALL the provided tagids will be loaded.
+ * @deprecated since Moodle 4.3
+ * @todo Final deprecation on Moodle 4.7 MDL-78091
*/
protected function ensure_questions_for_category_loaded($categoryid, $includesubcategories, $tagids = []): void {
+ debugging(
+ 'Function ensure_questions_for_category_loaded() is deprecated, please use the function ' .
+ 'ensure_filtered_questions_loaded.',
+ DEBUG_DEVELOPER
+ );
+
global $DB;
$categorykey = $this->get_category_key($categoryid, $includesubcategories, $tagids);
@@ -213,6 +369,26 @@ class random_question_loader {
}
}
+ /**
+ * Get filtered questions.
+ *
+ * @param array $filters filter array
+ * @return array list of filtered questions
+ */
+ protected function get_filtered_question_ids(array $filters): array {
+ $this->ensure_filtered_questions_loaded($filters);
+ $key = $this->get_filtered_questions_key($filters);
+
+ $cachedvalues = $this->availablequestionscache[$key];
+ $questionids = [];
+
+ foreach ($cachedvalues as $usecount => $ids) {
+ $questionids = array_merge($questionids, array_keys($ids));
+ }
+
+ return $questionids;
+ }
+
/**
* Get the list of available question ids for the given criteria.
*
@@ -222,8 +398,15 @@ class random_question_loader {
* @param array $tagids An array of tag ids. If an array is provided, then
* only the questions that are tagged with ALL the provided tagids will be loaded.
* @return int[] The list of question ids
+ * @deprecated since Moodle 4.3
+ * @todo Final deprecation on Moodle 4.7 MDL-78091
*/
protected function get_question_ids($categoryid, $includesubcategories, $tagids = []): array {
+ debugging(
+ 'Function get_question_ids() is deprecated, please use get_filtered_question_ids() instead.',
+ DEBUG_DEVELOPER
+ );
+
$this->ensure_questions_for_category_loaded($categoryid, $includesubcategories, $tagids);
$categorykey = $this->get_category_key($categoryid, $includesubcategories, $tagids);
$cachedvalues = $this->availablequestionscache[$categorykey];
@@ -236,6 +419,29 @@ class random_question_loader {
return $questionids;
}
+ /**
+ * Check whether a given question is available in a given category. If so, mark it used.
+ * If an optional list of tag ids are provided, then the question must be tagged with
+ * ALL of the provided tags to be considered as available.
+ *
+ * @param array $filters filter array
+ * @param int $questionid the question that is being used.
+ * @return bool whether the question is available in the requested category.
+ */
+ public function is_filtered_question_available(array $filters, int $questionid): bool {
+ $this->ensure_filtered_questions_loaded($filters);
+ $categorykey = $this->get_filtered_questions_key($filters);
+
+ foreach ($this->availablequestionscache[$categorykey] as $questionids) {
+ if (isset($questionids[$questionid])) {
+ $this->use_question($questionid);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
/**
* Check whether a given question is available in a given category. If so, mark it used.
* If an optional list of tag ids are provided, then the question must be tagged with
@@ -247,8 +453,14 @@ class random_question_loader {
* @param int $questionid the question that is being used.
* @param array $tagids An array of tag ids. Only the questions that are tagged with all the provided tagids can be available.
* @return bool whether the question is available in the requested category.
+ * @deprecated since Moodle 4.3
+ * @todo Final deprecation on Moodle 4.7 MDL-78091
*/
public function is_question_available($categoryid, $includesubcategories, $questionid, $tagids = []): bool {
+ debugging(
+ 'Function is_question_available() is deprecated, please use is_filtered_question_available() instead.',
+ DEBUG_DEVELOPER
+ );
$this->ensure_questions_for_category_loaded($categoryid, $includesubcategories, $tagids);
$categorykey = $this->get_category_key($categoryid, $includesubcategories, $tagids);
@@ -262,6 +474,52 @@ class random_question_loader {
return false;
}
+ /**
+ * Get the list of available questions for the given criteria.
+ *
+ * @param array $filters filter array
+ * @param int $limit Maximum number of results to return.
+ * @param int $offset Number of items to skip from the begging of the result set.
+ * @param string[] $fields The fields to return for each question.
+ * @return \stdClass[] The list of question records
+ */
+ public function get_filtered_questions($filters, $limit = 100, $offset = 0, $fields = []) {
+ global $DB;
+
+ $questionids = $this->get_filtered_question_ids($filters);
+
+ if (empty($questionids)) {
+ return [];
+ }
+
+ if (empty($fields)) {
+ // Return all fields.
+ $fieldsstring = '*';
+ } else {
+ $fieldsstring = implode(',', $fields);
+ }
+
+ // Create the query to get the questions (validate that at least we have a question id. If not, do not execute the sql).
+ $hasquestions = false;
+ if (!empty($questionids)) {
+ $hasquestions = true;
+ }
+ if ($hasquestions) {
+ [$condition, $param] = $DB->get_in_or_equal($questionids, SQL_PARAMS_NAMED, 'questionid');
+ $condition = 'WHERE q.id ' . $condition;
+ $sql = "SELECT {$fieldsstring}
+ FROM (SELECT q.*, qbe.questioncategoryid as category
+ FROM {question} q
+ JOIN {question_versions} qv ON qv.questionid = q.id
+ JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
+ {$condition}) q";
+
+ return $DB->get_records_sql($sql, $param, $offset, $limit);
+ } else {
+ return [];
+ }
+ }
+
/**
* Get the list of available questions for the given criteria.
*
@@ -274,8 +532,14 @@ class random_question_loader {
* @param int $offset Number of items to skip from the begging of the result set.
* @param string[] $fields The fields to return for each question.
* @return \stdClass[] The list of question records
+ * @deprecated since Moodle 4.3
+ * @todo Final deprecation on Moodle 4.7 MDL-78091
*/
public function get_questions($categoryid, $includesubcategories, $tagids = [], $limit = 100, $offset = 0, $fields = []) {
+ debugging(
+ 'Function get_questions() is deprecated, please use get_filtered_questions() instead.',
+ DEBUG_DEVELOPER
+ );
global $DB;
$questionids = $this->get_question_ids($categoryid, $includesubcategories, $tagids);
@@ -311,6 +575,17 @@ class random_question_loader {
}
}
+ /**
+ * Count number of filtered questions
+ *
+ * @param array $filters filter array
+ * @return int number of question
+ */
+ public function count_filtered_questions(array $filters): int {
+ $questionids = $this->get_filtered_question_ids($filters);
+ return count($questionids);
+ }
+
/**
* Count the number of available questions for the given criteria.
*
@@ -320,8 +595,14 @@ class random_question_loader {
* @param array $tagids An array of tag ids. If an array is provided, then
* only the questions that are tagged with ALL the provided tagids will be loaded.
* @return int The number of questions matching the criteria.
+ * @deprecated since Moodle 4.3
+ * @todo Final deprecation on Moodle 4.7 MDL-78091
*/
public function count_questions($categoryid, $includesubcategories, $tagids = []): int {
+ debugging(
+ 'Function count_questions() is deprecated, please use count_filtered_questions() instead.',
+ DEBUG_DEVELOPER
+ );
$questionids = $this->get_question_ids($categoryid, $includesubcategories, $tagids);
return count($questionids);
}
diff --git a/question/classes/local/bank/view.php b/question/classes/local/bank/view.php
index db25b885719..f072c225455 100644
--- a/question/classes/local/bank/view.php
+++ b/question/classes/local/bank/view.php
@@ -24,17 +24,21 @@
namespace core_question\local\bank;
-use core\plugininfo\qbank;
-use core_plugin_manager;
-use core_question\bank\search\condition;
-use core_question\local\statistics\statistics_bulk_loader;
-use qbank_columnsortorder\column_manager;
-use qbank_editquestion\editquestion_helper;
-
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/question/editlib.php');
+use core\plugininfo\qbank;
+use core\output\datafilter;
+use core_plugin_manager;
+use core_question\local\bank\condition;
+use core_question\local\statistics\statistics_bulk_loader;
+use core_question\output\question_bank_filter_ui;
+use qbank_columnsortorder\column_manager;
+use qbank_deletequestion\hidden_condition;
+use qbank_editquestion\editquestion_helper;
+use qbank_managecategories\category_condition;
+
/**
* This class prints a view of the question bank.
*
@@ -79,7 +83,7 @@ class view {
/**
* @var \core_question\local\bank\question_edit_contexts
*/
- protected $contexts;
+ public $contexts;
/**
* @var object|\cm_info|null if we are in a module context, the cm.
@@ -110,6 +114,12 @@ class view {
*/
protected $visiblecolumns;
+ /**
+ * @var column_base[] these are the 'columns' that are
+ * common to the question bank.
+ */
+ protected $corequestionbankcolumns;
+
/**
* @var column_base[] these are the 'columns' that are
* actually displayed as an additional row (e.g. question text), in order.
@@ -167,34 +177,83 @@ class view {
*/
public $returnurl;
- /**
- * @var bool enable or disable filters while calling the API.
- */
- public $enablefilters = true;
-
- /**
- * @var array to pass custom filters instead of the specified ones.
- */
- public $customfilterobjects = null;
-
/**
* @var array $bulkactions to identify the bulk actions for the api.
*/
public $bulkactions = [];
+ /**
+ * @var int|null Number of questions.
+ */
+ protected $totalcount = null;
+
+ /**
+ * @var array Parameters for the page URL.
+ */
+ protected $pagevars = [];
+
+ /**
+ * @var array $plugins all the qbank plugin objects.
+ */
+ protected $plugins = [];
+
+ /**
+ * @var string $component the component the api is used from.
+ */
+ public $component = 'core_question';
+
+ /**
+ * @var string $callback name of the callback for the api call via filter js.
+ */
+ public $callback = 'question_data';
+
+ /**
+ * @var array $extraparams extra parameters for the extended apis.
+ */
+ public $extraparams = [];
+
/**
* Constructor for view.
*
* @param \core_question\local\bank\question_edit_contexts $contexts
* @param \moodle_url $pageurl
* @param object $course course settings
- * @param object $cm (optional) activity settings.
+ * @param null $cm (optional) activity settings.
+ * @param array $params the parameters required to initialize the api.
+ * @param array $extraparams any extra parameters required by a particular view class.
*/
- public function __construct($contexts, $pageurl, $course, $cm = null) {
+ public function __construct($contexts, $pageurl, $course, $cm = null, $params = [], $extraparams = []) {
$this->contexts = $contexts;
$this->baseurl = $pageurl;
$this->course = $course;
$this->cm = $cm;
+ $this->extraparams = $extraparams;
+
+ // Default filter condition.
+ if (!isset($params['filter']) && isset($params['cat'])) {
+ $params['filter'] = [];
+ [$categoryid, $contextid] = category_condition::validate_category_param($params['cat']);
+ if (!is_null($categoryid)) {
+ $category = category_condition::get_category_record($categoryid, $contextid);
+ $params['filter']['category'] = [
+ 'jointype' => category_condition::JOINTYPE_DEFAULT,
+ 'values' => [$category->id],
+ 'filteroptions' => ['includesubcategories' => false],
+ ];
+ }
+ $params['filter']['hidden'] = [
+ 'jointype' => hidden_condition::JOINTYPE_DEFAULT,
+ 'values' => [0],
+ ];
+ $params['jointype'] = datafilter::JOINTYPE_ALL;
+ }
+ if (!empty($params['filter'])) {
+ $params['filter'] = filter_condition_manager::unpack_filteroptions_param($params['filter']);
+ }
+ if (isset($params['filter']['jointype'])) {
+ $params['jointype'] = $params['filter']['jointype'];
+ unset($params['filter']['jointype']);
+ }
// Create the url of the new question page to forward to.
$this->returnurl = $pageurl->out_as_local_url(false);
@@ -205,13 +264,14 @@ class view {
$this->editquestionurl->param('courseid', $this->course->id);
}
- $this->lastchangedid = optional_param('lastchanged', 0, PARAM_INT);
+ $this->lastchangedid = clean_param($pageurl->param('lastchanged'), PARAM_INT);
// Possibly the heading part can be removed.
+ $this->plugins = \core_component::get_plugin_list_with_class('qbank', 'plugin_feature', 'plugin_feature.php');
+ $this->set_pagevars($params);
$this->init_columns($this->wanted_columns(), $this->heading_column());
$this->init_question_actions();
$this->init_sort();
- $this->init_search_conditions();
$this->init_bulk_actions();
}
@@ -250,8 +310,16 @@ class view {
* Initialize search conditions from plugins
* local_*_get_question_bank_search_conditions() must return an array of
* \core_question\bank\search\condition objects.
+ *
+ * @deprecated Since Moodle 4.3
+ * @todo Final deprecation on Moodle 4.7 MDL-78090
*/
protected function init_search_conditions(): void {
+ debugging(
+ 'Function init_search_conditions() has been deprecated, please create a qbank plugin' .
+ 'and implement a filter object instead.',
+ DEBUG_DEVELOPER
+ );
$searchplugins = get_plugin_list_with_function('local', 'get_question_bank_search_conditions');
foreach ($searchplugins as $component => $function) {
foreach ($function($this) as $searchobject) {
@@ -283,28 +351,29 @@ class view {
}
/**
- * Get the list of qbank plugins with available objects for features.
+ * Get class for each question bank columns.
*
* @return array
*/
- protected function get_question_bank_plugins(): array {
- $questionbankclasscolumns = [];
- $newpluginclasscolumns = [];
- $corequestionbankcolumns = [
- 'checkbox_column',
- 'question_type_column',
- 'question_name_idnumber_tags_column',
- 'edit_menu_column',
- 'question_status_column',
- 'version_number_column',
- 'creator_name_column',
- 'comment_count_column'
+ protected function get_class_for_columns(): array {
+ $this->corequestionbankcolumns = [
+ 'checkbox_column',
+ 'question_type_column',
+ 'question_name_idnumber_tags_column',
+ 'edit_menu_column',
+ 'export_xml_action_column',
+ 'question_status_column',
+ 'version_number_column',
+ 'creator_name_column',
+ 'comment_count_column',
];
+
if (question_get_display_preference('qbshowtext', 0, PARAM_INT, new \moodle_url(''))) {
- $corequestionbankcolumns[] = 'question_text_row';
+ $this->corequestionbankcolumns[] = 'question_text_row';
}
- foreach ($corequestionbankcolumns as $fullname) {
+ $questionbankclasscolumns = [];
+ foreach ($this->corequestionbankcolumns as $fullname) {
$shortname = $fullname;
if (class_exists('core_question\\local\\bank\\' . $fullname)) {
$fullname = 'core_question\\local\\bank\\' . $fullname;
@@ -313,7 +382,19 @@ class view {
$questionbankclasscolumns[$shortname] = '';
}
}
- $plugins = \core_component::get_plugin_list_with_class('qbank', 'plugin_feature', 'plugin_feature.php');
+ return $questionbankclasscolumns;
+ }
+
+ /**
+ * Get the list of qbank plugins with available objects for features.
+ *
+ * @return array
+ */
+ protected function get_question_bank_plugins(): array {
+ $newpluginclasscolumns = [];
+ $questionbankclasscolumns = $this->get_class_for_columns();
+
+ $plugins = $this->plugins;
foreach ($plugins as $componentname => $plugin) {
$pluginentrypointobject = new $plugin();
$plugincolumnobjects = $pluginentrypointobject->get_question_columns($this);
@@ -324,7 +405,7 @@ class view {
}
foreach ($plugincolumnobjects as $columnobject) {
$columnname = $columnobject->get_column_name();
- foreach ($corequestionbankcolumns as $corequestionbankcolumn) {
+ foreach ($this->corequestionbankcolumns as $key => $corequestionbankcolumn) {
if (!\core\plugininfo\qbank::is_plugin_enabled($componentname)) {
unset($questionbankclasscolumns[$columnname]);
continue;
@@ -459,10 +540,21 @@ class view {
* Initialise sorting.
*/
protected function init_sort(): void {
- $this->init_sort_from_params();
- if (empty($this->sort)) {
- $this->sort = $this->default_sort();
+ $this->sort = [];
+ $sorts = optional_param_array('sortdata', [], PARAM_INT);
+ if (empty($sorts)) {
+ $sorts = $this->get_pagevars('sortdata');
}
+ if (empty($sorts)) {
+ $sorts = $this->default_sort();
+ }
+ $sorts = array_slice($sorts, 0, self::MAX_SORTS);
+ foreach ($sorts as $sortname => $sortorder) {
+ // Deal with subsorts.
+ [$colname] = $this->parse_subsort($sortname);
+ $this->get_column_type($colname);
+ }
+ $this->sort = $sorts;
}
/**
@@ -481,50 +573,24 @@ class view {
$colname = $sort;
$subsort = '';
}
+ $colname = str_replace('__', '\\', $colname);
// Validate the column name.
$this->get_column_type($colname);
$column = $this->requiredcolumns[$colname];
if (!isset($column) || !$column->is_sortable()) {
- for ($i = 1; $i <= self::MAX_SORTS; $i++) {
- $this->baseurl->remove_params('qbs' . $i);
- }
- throw new \moodle_exception('unknownsortcolumn', '', $link = $this->baseurl->out(), $colname);
+ $this->baseurl->remove_params('sortdata');
+ throw new \moodle_exception('unknownsortcolumn', '', $this->baseurl->out(), $colname);
}
// Validate the subsort, if present.
if ($subsort) {
$subsorts = $column->is_sortable();
if (!is_array($subsorts) || !isset($subsorts[$subsort])) {
- throw new \moodle_exception('unknownsortcolumn', '', $link = $this->baseurl->out(), $sort);
+ throw new \moodle_exception('unknownsortcolumn', '', $this->baseurl->out(), $sort);
}
}
return [$colname, $subsort];
}
- /**
- * Initialise sort from parameters.
- */
- protected function init_sort_from_params(): void {
- $this->sort = [];
- for ($i = 1; $i <= self::MAX_SORTS; $i++) {
- if (!$sort = optional_param('qbs' . $i, '', PARAM_TEXT)) {
- break;
- }
- // Work out the appropriate order.
- $order = 1;
- if ($sort[0] == '-') {
- $order = -1;
- $sort = substr($sort, 1);
- if (!$sort) {
- break;
- }
- }
- // Deal with subsorts.
- list($colname) = $this->parse_subsort($sort);
- $this->get_column_type($colname);
- $this->sort[$sort] = $order;
- }
- }
-
/**
* Sort to parameters.
*
@@ -533,13 +599,8 @@ class view {
*/
protected function sort_to_params($sorts): array {
$params = [];
- $i = 0;
- foreach ($sorts as $sort => $order) {
- $i += 1;
- if ($order < 0) {
- $sort = '-' . $sort;
- }
- $params['qbs' . $i] = $sort;
+ foreach ($sorts as $sortname => $sortorder) {
+ $params['sortdata[' . $sortname . ']'] = $sortorder;
}
return $params;
}
@@ -551,13 +612,11 @@ class view {
protected function default_sort(): array {
$defaultsort = [];
if (class_exists('\\qbank_viewquestiontype\\question_type_column')) {
- $sort = 'qbank_viewquestiontype\question_type_column';
+ $defaultsort['qbank_viewquestiontype__question_type_column'] = SORT_ASC;
}
- $defaultsort[$sort] = 1;
if (class_exists('\\qbank_viewquestionname\\question_name_idnumber_tags_column')) {
- $sort = 'qbank_viewquestionname\question_name_idnumber_tags_column';
+ $defaultsort['qbank_viewquestionname__question_name_idnumber_tags_column-name'] = SORT_ASC;
}
- $defaultsort[$sort . '-name'] = 1;
return $defaultsort;
}
@@ -565,38 +624,33 @@ class view {
/**
* Gets the primary sort order according to the default sort.
*
- * @param string $sort a column or column_subsort name.
+ * @param string $sortname a column or column_subsort name.
* @return int the current sort order for this column -1, 0, 1
*/
- public function get_primary_sort_order($sort): int {
+ public function get_primary_sort_order($sortname): int {
$order = reset($this->sort);
$primarysort = key($this->sort);
- if ($sort == $primarysort) {
+ if ($sortname == $primarysort) {
return $order;
- } else {
- return 0;
}
+
+ return 0;
}
/**
* Get a URL to redisplay the page with a new sort for the question bank.
*
- * @param string $sort the column, or column_subsort to sort on.
+ * @param string $sortname the column, or column_subsort to sort on.
* @param bool $newsortreverse whether to sort in reverse order.
* @return string The new URL.
*/
- public function new_sort_url($sort, $newsortreverse): string {
- if ($newsortreverse) {
- $order = -1;
- } else {
- $order = 1;
- }
+ public function new_sort_url($sortname, $newsortreverse): string {
// Tricky code to add the new sort at the start, removing it from where it was before, if it was present.
$newsort = array_reverse($this->sort);
- if (isset($newsort[$sort])) {
- unset($newsort[$sort]);
+ if (isset($newsort[$sortname])) {
+ unset($newsort[$sortname]);
}
- $newsort[$sort] = $order;
+ $newsort[$sortname] = $newsortreverse ? SORT_DESC : SORT_ASC;
$newsort = array_reverse($newsort);
if (count($newsort) > self::MAX_SORTS) {
$newsort = array_slice($newsort, 0, self::MAX_SORTS, true);
@@ -679,9 +733,9 @@ class view {
// Build the order by clause.
$sorts = [];
- foreach ($this->sort as $sort => $order) {
- list($colname, $subsort) = $this->parse_subsort($sort);
- $sorts[] = $this->requiredcolumns[$colname]->sort_expression($order < 0, $subsort);
+ foreach ($this->sort as $sortname => $sortorder) {
+ [$colname, $subsort] = $this->parse_subsort($sortname);
+ $sorts[] = $this->requiredcolumns[$colname]->sort_expression($sortorder == SORT_DESC, $subsort);
}
// Build the where clause.
@@ -690,52 +744,66 @@ class view {
JOIN {question_bank_entries} be
ON be.id = v.questionbankentryid
WHERE be.id = qbe.id)';
- $tests = ['q.parent = 0', $latestversion];
$this->sqlparams = [];
+ $conditions = [];
foreach ($this->searchconditions as $searchcondition) {
if ($searchcondition->where()) {
- $tests[] = '((' . $searchcondition->where() .'))';
+ $conditions[] = '((' . $searchcondition->where() .'))';
}
if ($searchcondition->params()) {
$this->sqlparams = array_merge($this->sqlparams, $searchcondition->params());
}
}
+ // Get higher level filter condition.
+ $jointype = isset($this->pagevars['jointype']) ? (int)$this->pagevars['jointype'] : condition::JOINTYPE_DEFAULT;
+ $nonecondition = ($jointype === datafilter::JOINTYPE_NONE) ? ' NOT ' : '';
+ $separator = ($jointype === datafilter::JOINTYPE_ALL) ? ' AND ' : ' OR ';
// Build the SQL.
$sql = ' FROM {question} q ' . implode(' ', $joins);
- $sql .= ' WHERE ' . implode(' AND ', $tests);
+ $sql .= ' WHERE q.parent = 0 AND ' . $latestversion;
+ if (!empty($conditions)) {
+ $sql .= ' AND ' . $nonecondition . ' ( ';
+ $sql .= implode($separator, $conditions);
+ $sql .= ' ) ';
+ }
$this->countsql = 'SELECT count(1)' . $sql;
$this->loadsql = 'SELECT ' . implode(', ', $fields) . $sql . ' ORDER BY ' . implode(', ', $sorts);
}
/**
* Get the number of questions.
+ *
* @return int
*/
- protected function get_question_count(): int {
+ public function get_question_count(): int {
global $DB;
- return $DB->count_records_sql($this->countsql, $this->sqlparams);
+ if (is_null($this->totalcount)) {
+ $this->totalcount = $DB->count_records_sql($this->countsql, $this->sqlparams);
+ }
+ return $this->totalcount;
}
/**
* Load the questions we need to display.
*
- * @param int $page page to display.
- * @param int $perpage number of questions per page.
* @return \moodle_recordset questionid => data about each question.
*/
- protected function load_page_questions($page, $perpage): \moodle_recordset {
+ protected function load_page_questions(): \moodle_recordset {
global $DB;
- $questions = $DB->get_recordset_sql($this->loadsql, $this->sqlparams, $page * $perpage, $perpage);
+ $questions = $DB->get_recordset_sql($this->loadsql, $this->sqlparams,
+ (int)$this->pagevars['qpage'] * (int)$this->pagevars['qperpage'], $this->pagevars['qperpage']);
if (empty($questions)) {
$questions->close();
// No questions on this page. Reset to page 0.
- $questions = $DB->get_recordset_sql($this->loadsql, $this->sqlparams, 0, $perpage);
+ $questions = $DB->get_recordset_sql($this->loadsql, $this->sqlparams, 0, $this->pagevars['qperpage']);
}
return $questions;
}
/**
* Returns the base url.
+ *
+ * @return \moodle_url
*/
public function base_url(): \moodle_url {
return $this->baseurl;
@@ -797,97 +865,89 @@ class view {
* @todo Final deprecation on Moodle 4.4 MDL-72438
*/
public function preview_question_url($questiondata) {
- debugging('Function preview_question_url() has been deprecated and moved to qbank_previewquestion plugin,
- please use qbank_previewquestion\helper::question_preview_url() instead.', DEBUG_DEVELOPER);
+ debugging(
+ 'Function preview_question_url() has been deprecated and moved to qbank_previewquestion plugin, ' .
+ 'please use qbank_previewquestion\helper::question_preview_url() instead.',
+ DEBUG_DEVELOPER
+ );
return question_preview_url($questiondata->id, null, null, null, null,
- $this->get_most_specific_context());
+ $this->get_most_specific_context());
+ }
+
+ /**
+ * Get fields from the pagevars array.
+ *
+ * If a field is specified, that particlar pagevars field will be returned. Otherwise the entire array will be returned.
+ *
+ * If a field is specified but it does not exist, null will be returned.
+ *
+ * @param ?string $field
+ * @return mixed
+ */
+ public function get_pagevars(?string $field = null): mixed {
+ if (is_null($field)) {
+ return $this->pagevars;
+ } else {
+ return $this->pagevars[$field] ?? null;
+ }
+ }
+
+ /**
+ * Set the pagevars property with the provided array.
+ *
+ * @param array $pagevars
+ */
+ public function set_pagevars(array $pagevars): void {
+ $this->pagevars = $pagevars;
}
/**
* Shows the question bank interface.
- *
- * The function also processes a number of actions:
- *
- * Actions affecting the question pool:
- * move Moves a question to a different category
- * deleteselected Deletes the selected questions from the category
- * Other actions:
- * category Chooses the category
- * params: $tabname question bank edit tab name, for permission checking
- * $pagevars current list of page variables
- *
- * @param string $tabname
- * @param array $pagevars
*/
- public function display($pagevars, $tabname): void {
-
- $page = $pagevars['qpage'];
- $perpage = $pagevars['qperpage'];
- $cat = $pagevars['cat'];
- $recurse = $pagevars['recurse'];
- $showhidden = $pagevars['showhidden'];
- $showquestiontext = $pagevars['qbshowtext'];
- $tagids = [];
- if (!empty($pagevars['qtagids'])) {
- $tagids = $pagevars['qtagids'];
- }
-
+ public function display(): void {
echo \html_writer::start_div('questionbankwindow boxwidthwide boxaligncenter');
-
- $editcontexts = $this->contexts->having_one_edit_tab_cap($tabname);
-
// Show the filters and search options.
- $this->wanted_filters($cat, $tagids, $showhidden, $recurse, $editcontexts, $showquestiontext);
-
+ $this->wanted_filters();
// Continues with list of questions.
- $this->display_question_list($this->baseurl, $cat, null, $page, $perpage,
- $this->contexts->having_cap('moodle/question:add'));
+ $this->display_question_list();
echo \html_writer::end_div();
}
/**
* The filters for the question bank.
- *
- * @param string $cat 'categoryid,contextid'
- * @param array $tagids current list of selected tags
- * @param bool $showhidden whether deleted questions should be displayed
- * @param int $recurse Whether to include subcategories
- * @param array $editcontexts parent contexts
- * @param bool $showquestiontext whether the text of each question should be shown in the list
*/
- public function wanted_filters($cat, $tagids, $showhidden, $recurse, $editcontexts, $showquestiontext): void {
- global $CFG;
- list(, $contextid) = explode(',', $cat);
+ public function wanted_filters(): void {
+ global $OUTPUT;
+ [, $contextid] = explode(',', $this->pagevars['cat']);
$catcontext = \context::instance_by_id($contextid);
- $thiscontext = $this->get_most_specific_context();
// Category selection form.
$this->display_question_bank_header();
-
- // Display tag filter if usetags setting is enabled/enablefilters is true.
- if ($this->enablefilters) {
- if (is_array($this->customfilterobjects)) {
- foreach ($this->customfilterobjects as $filterobjects) {
- $this->searchconditions[] = $filterobjects;
- }
- } else {
- if ($CFG->usetags) {
- array_unshift($this->searchconditions,
- new \core_question\bank\search\tag_condition([$catcontext, $thiscontext], $tagids));
- }
-
- array_unshift($this->searchconditions, new \core_question\bank\search\hidden_condition(!$showhidden));
- array_unshift($this->searchconditions, new \core_question\bank\search\category_condition(
- $cat, $recurse, $editcontexts, $this->baseurl, $this->course));
- }
- }
- $this->display_options_form($showquestiontext);
+ // Add search conditions.
+ $this->add_standard_search_conditions();
+ // Render the question bank filters.
+ $additionalparams = [
+ 'perpage' => $this->pagevars['qperpage'],
+ ];
+ $filter = new question_bank_filter_ui($catcontext, $this->searchconditions, $additionalparams, $this->component,
+ $this->callback, static::class, 'qbank-table', $this->cm?->id, $this->pagevars,
+ $this->extraparams);
+ echo $OUTPUT->render($filter);
}
/**
* Print the text if category id not available.
+ *
+ * @deprecated since Moodle 4.3 MDL-72321
+ * @todo Final deprecation on Moodle 4.7 MDL-78090
*/
protected function print_choose_category_message(): void {
+ debugging(
+ 'Function print_choose_category_message() is deprecated, all the features for this method is currently ' .
+ 'handled by the qbank filter api, please have a look at ' .
+ 'question/bank/managecategories/classes/category_confition.php for more information.',
+ DEBUG_DEVELOPER
+ );
echo \html_writer::start_tag('p', ['style' => "\"text-align:center;\""]);
echo \html_writer::tag('b', get_string('selectcategoryabove', 'question'));
echo \html_writer::end_tag('p');
@@ -897,8 +957,17 @@ class view {
* Gets current selected category.
* @param string $categoryandcontext
* @return false|mixed|\stdClass
+ *
+ * @deprecated since Moodle 4.3 MDL-72321
+ * @todo Final deprecation on Moodle 4.7 MDL-78090
*/
protected function get_current_category($categoryandcontext) {
+ debugging(
+ 'Function get_current_category() is deprecated, all the features for this method is currently handled by ' .
+ 'the qbank filter api, please have a look at question/bank/managecategories/classes/category_confition.php ' .
+ 'for more information.',
+ DEBUG_DEVELOPER
+ );
global $DB, $OUTPUT;
list($categoryid, $contextid) = explode(',', $categoryandcontext);
if (!$categoryid) {
@@ -907,7 +976,7 @@ class view {
}
if (!$category = $DB->get_record('question_categories',
- ['id' => $categoryid, 'contextid' => $contextid])) {
+ ['id' => $categoryid, 'contextid' => $contextid])) {
echo $OUTPUT->box_start('generalbox questionbank');
echo $OUTPUT->notification('Category not found!');
echo $OUTPUT->box_end();
@@ -921,13 +990,20 @@ class view {
* Display the form with options for which questions are displayed and how they are displayed.
*
* @param bool $showquestiontext Display the text of the question within the list.
+ * @deprecated since Moodle 4.3 MDL-72321
+ * @todo Final deprecation on Moodle 4.7 MDL-78090
*/
protected function display_options_form($showquestiontext): void {
+ debugging(
+ 'Function display_options_form() is deprecated, this method has been replaced with mustaches in filters, ' .
+ 'please use filtering objects',
+ DEBUG_DEVELOPER
+ );
global $PAGE;
// The html will be refactored in the filter feature implementation.
echo \html_writer::start_tag('form', ['method' => 'get',
- 'action' => new \moodle_url($this->baseurl), 'id' => 'displayoptions']);
+ 'action' => new \moodle_url($this->baseurl), 'id' => 'displayoptions']);
echo \html_writer::start_div();
$excludes = ['recurse', 'showhidden', 'qbshowtext'];
@@ -949,9 +1025,7 @@ class view {
if ($searchcondition->display_options_adv()) {
$advancedsearch[] = $searchcondition;
}
- echo $searchcondition->display_options();
}
- $this->display_showtext_checkbox($showquestiontext);
if (!empty($advancedsearch)) {
$this->display_advanced_search_form($advancedsearch);
}
@@ -967,11 +1041,18 @@ class view {
* Print the "advanced" UI elements for the form to select which questions. Hidden by default.
*
* @param array $advancedsearch
+ * @deprecated since Moodle 4.3 MDL-72321
+ * @todo Final deprecation on Moodle 4.7 MDL-78090
*/
protected function display_advanced_search_form($advancedsearch): void {
+ debugging(
+ 'Function display_advanced_search_form() is deprecated, this method has been replaced with mustaches in ' .
+ 'filters, please use filtering objects',
+ DEBUG_DEVELOPER
+ );
print_collapsible_region_start('', 'advancedsearch',
- get_string('advancedsearchoptions', 'question'),
- 'question_bank_advanced_search');
+ get_string('advancedsearchoptions', 'question'),
+ 'question_bank_advanced_search');
foreach ($advancedsearch as $searchcondition) {
echo $searchcondition->display_options_adv();
}
@@ -981,11 +1062,14 @@ class view {
/**
* Display the checkbox UI for toggling the display of the question text in the list.
* @param bool $showquestiontext the current or default value for whether to display the text.
+ * @deprecated since Moodle 4.3 MDL-72321
+ * @todo Final deprecation on Moodle 4.7 MDL-78090
*/
protected function display_showtext_checkbox($showquestiontext): void {
+ debugging('Function display_showtext_checkbox() is deprecated, please use filtering objects', DEBUG_DEVELOPER);
global $PAGE;
$displaydata = [
- 'checked' => $showquestiontext
+ 'checked' => $showquestiontext
];
if (class_exists('qbank_viewquestiontext\\question_text_row')) {
if (\core\plugininfo\qbank::is_plugin_enabled('qbank_viewquestiontext')) {
@@ -1011,44 +1095,30 @@ class view {
protected function create_new_question_form($category, $canadd): void {
if (\core\plugininfo\qbank::is_plugin_enabled('qbank_editquestion')) {
echo editquestion_helper::create_new_question_button($category->id,
- $this->questionactions['qbank_editquestion\edit_action']->editquestionurl->params(), $canadd);
+ $this->questionactions['qbank_editquestion\edit_action']->editquestionurl->params(), $canadd);
}
}
/**
* Prints the table of questions in a category with interactions
- *
- * @param \moodle_url $pageurl The URL to reload this page.
- * @param string $categoryandcontext 'categoryID,contextID'.
- * @param int $recurse Whether to include subcategories.
- * @param int $page The number of the page to be displayed
- * @param int|null $perpage Number of questions to show per page
- * @param array $addcontexts contexts where the user is allowed to add new questions.
*/
- protected function display_question_list($pageurl, $categoryandcontext, $recurse = 1, $page = 0,
- $perpage = null, $addcontexts = []): void {
- global $OUTPUT;
+ protected function display_question_list(): void {
// This function can be moderately slow with large question counts and may time out.
// We probably do not want to raise it to unlimited, so randomly picking 5 minutes.
// Note: We do not call this in the loop because quiz ob_ captures this function (see raise() PHP doc).
\core_php_time_limit::raise(300);
- $category = $this->get_current_category($categoryandcontext);
- $perpage = $perpage ?? $this->pagesize;
-
- list($categoryid, $contextid) = explode(',', $categoryandcontext);
+ [$categoryid, $contextid] = category_condition::validate_category_param($this->pagevars['cat']);
$catcontext = \context::instance_by_id($contextid);
$canadd = has_capability('moodle/question:add', $catcontext);
+ $category = category_condition::get_category_record($categoryid, $contextid);
$this->create_new_question_form($category, $canadd);
$this->build_query();
- $totalnumber = $this->get_question_count();
- if ($totalnumber == 0) {
- return;
- }
- $questionsrs = $this->load_page_questions($page, $perpage);
+ $questionsrs = $this->load_page_questions();
+ $totalquestions = $this->get_question_count();
$questions = [];
foreach ($questionsrs as $question) {
if (!empty($question->id)) {
@@ -1057,29 +1127,25 @@ class view {
}
$questionsrs->close();
- // Bulk load any required statistics.
- $this->load_required_statistics($questions);
-
- // Bulk load any extra data that any column requires.
- foreach ($this->requiredcolumns as $name => $column) {
- $column->load_additional_data($questions);
- }
-
- $pageingurl = new \moodle_url($pageurl, $pageurl->params());
- $pagingbar = new \paging_bar($totalnumber, $page, $perpage, $pageingurl);
- $pagingbar->pagevar = 'qpage';
-
- $this->display_top_pagnation($OUTPUT->render($pagingbar));
-
// This html will be refactored in the bulk actions implementation.
- echo \html_writer::start_tag('form', ['action' => $pageurl, 'method' => 'post', 'id' => 'questionsubmit']);
+ echo \html_writer::start_tag('form', ['action' => $this->baseurl, 'method' => 'post', 'id' => 'questionsubmit']);
echo \html_writer::start_tag('fieldset', ['class' => 'invisiblefieldset', 'style' => "display: block;"]);
echo \html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey()]);
echo \html_writer::input_hidden_params($this->baseurl);
- $this->display_questions($questions);
+ echo \html_writer::start_tag('div',
+ ['class' => 'categoryquestionscontainer', 'id' => 'questionscontainer']);
+ if ($totalquestions > 0) {
+ // Bulk load any required statistics.
+ $this->load_required_statistics($questions);
- $this->display_bottom_pagination($OUTPUT->render($pagingbar), $totalnumber, $perpage, $pageurl);
+ // Bulk load any extra data that any column requires.
+ foreach ($this->requiredcolumns as $column) {
+ $column->load_additional_data($questions);
+ }
+ $this->display_questions($questions, $this->pagevars['qpage'], $this->pagevars['qperpage']);
+ }
+ echo \html_writer::end_tag('div');
$this->display_bottom_controls($catcontext);
@@ -1145,11 +1211,17 @@ class view {
* Display the top pagination bar.
*
* @param object $pagination
+ * @deprecated since Moodle 4.3
+ * @todo Final deprecation on Moodle 4.7 MDL-78091
*/
- protected function display_top_pagnation($pagination): void {
+ public function display_top_pagnation($pagination): void {
+ debugging(
+ 'Function display_top_pagnation() is deprecated, please use display_questions() for ajax based pagination.',
+ DEBUG_DEVELOPER
+ );
global $PAGE;
$displaydata = [
- 'pagination' => $pagination
+ 'pagination' => $pagination
];
echo $PAGE->get_renderer('core_question', 'bank')->render_question_pagination($displaydata);
}
@@ -1161,19 +1233,25 @@ class view {
* @param int $totalnumber
* @param int $perpage
* @param \moodle_url $pageurl
+ * @deprecated since Moodle 4.3
+ * @todo Final deprecation on Moodle 4.7 MDL-78091
*/
- protected function display_bottom_pagination($pagination, $totalnumber, $perpage, $pageurl): void {
+ public function display_bottom_pagination($pagination, $totalnumber, $perpage, $pageurl): void {
+ debugging(
+ 'Function display_bottom_pagination() is deprecated, please use display_questions() for ajax based pagination.',
+ DEBUG_DEVELOPER
+ );
global $PAGE;
$displaydata = array (
- 'extraclasses' => 'pagingbottom',
- 'pagination' => $pagination,
- 'biggertotal' => true,
+ 'extraclasses' => 'pagingbottom',
+ 'pagination' => $pagination,
+ 'biggertotal' => true,
);
if ($totalnumber > $this->pagesize) {
$displaydata['showall'] = true;
if ($perpage == $this->pagesize) {
$url = new \moodle_url($pageurl, array_merge($pageurl->params(),
- ['qpage' => 0, 'qperpage' => MAXIMUM_QUESTIONS_PER_PAGE]));
+ ['qpage' => 0, 'qperpage' => MAXIMUM_QUESTIONS_PER_PAGE]));
if ($totalnumber > MAXIMUM_QUESTIONS_PER_PAGE) {
$displaydata['totalnumber'] = MAXIMUM_QUESTIONS_PER_PAGE;
} else {
@@ -1182,7 +1260,7 @@ class view {
}
} else {
$url = new \moodle_url($pageurl, array_merge($pageurl->params(),
- ['qperpage' => $this->pagesize]));
+ ['qperpage' => $this->pagesize]));
$displaydata['totalnumber'] = $this->pagesize;
}
$displaydata['showallurl'] = $url;
@@ -1203,7 +1281,8 @@ class view {
global $PAGE;
$bulkactiondatas = [];
$params = $this->base_url()->params();
- $params['returnurl'] = $this->base_url();
+ $returnurl = new \moodle_url($this->base_url(), ['filter' => json_encode($this->pagevars['filter'])]);
+ $params['returnurl'] = $returnurl;
foreach ($this->bulkactions as $key => $action) {
// Check capabilities.
$capcount = 0;
@@ -1237,11 +1316,49 @@ class view {
*
* @param array $questions
*/
- protected function display_questions($questions): void {
+ public function display_questions($questions, $page = 0, $perpage = DEFAULT_QUESTIONS_PER_PAGE): void {
+ global $OUTPUT;
+ if (!isset($this->pagevars['filter']['category'])) {
+ // We must have a category filter selected.
+ echo $OUTPUT->render_from_template('qbank_managecategories/choose_category', []);
+ return;
+ }
+ // Pagination.
+ $pageingurl = new \moodle_url($this->base_url());
+ $pagingbar = new \paging_bar($this->totalcount, $page, $perpage, $pageingurl);
+ $pagingbar->pagevar = 'qpage';
+ echo $OUTPUT->render($pagingbar);
+
+ // Table of questions.
+ // Embeded filterconditon into the div.
+ $filtercondition = json_encode($this->get_pagevars());
+
echo \html_writer::start_tag('div',
- ['class' => 'categoryquestionscontainer', 'id' => 'questionscontainer']);
+ ['class' => 'question_table', 'id' => 'question_table', 'data-filtercondition' => $filtercondition]);
$this->print_table($questions);
echo \html_writer::end_tag('div');
+ echo $OUTPUT->render($pagingbar);
+ }
+
+ /**
+ * Load the questions according to the search conditions.
+ *
+ * @return array
+ */
+ public function load_questions() {
+ $this->build_query();
+ $questionsrs = $this->load_page_questions();
+ $questions = [];
+ foreach ($questionsrs as $question) {
+ if (!empty($question->id)) {
+ $questions[$question->id] = $question;
+ }
+ }
+ $questionsrs->close();
+ foreach ($this->requiredcolumns as $name => $column) {
+ $column->load_additional_data($questions);
+ }
+ return $questions;
}
/**
@@ -1251,7 +1368,11 @@ class view {
*/
protected function print_table($questions): void {
// Start of the table.
- echo \html_writer::start_tag('table', ['id' => 'categoryquestions', 'class' => 'table-responsive']);
+ echo \html_writer::start_tag('table', [
+ 'id' => 'categoryquestions',
+ 'class' => 'table-responsive',
+ 'data-defaultsort' => json_encode($this->sort)
+ ]);
// Prints the table header.
echo \html_writer::start_tag('thead');
@@ -1276,9 +1397,9 @@ class view {
/**
* Start of the table html.
*
- * @deprecated since Moodle 4.0
* @see print_table()
- * @todo Final deprecation on Moodle 4.4 MDL-72438
+ * @deprecated since Moodle 4.3 MDL-72321
+ * @todo Final deprecation on Moodle 4.7 MDL-78090
*/
protected function start_table() {
debugging('Function start_table() is deprecated, please use print_table() instead.', DEBUG_DEVELOPER);
@@ -1292,9 +1413,9 @@ class view {
/**
* End of the table html.
*
- * @deprecated since Moodle 4.0
* @see print_table()
- * @todo Final deprecation on Moodle 4.4 MDL-72438
+ * @deprecated since Moodle 4.3 MDL-72321
+ * @todo Final deprecation on Moodle 4.7 MDL-78090
*/
protected function end_table() {
debugging('Function end_table() is deprecated, please use print_table() instead.', DEBUG_DEVELOPER);
@@ -1305,7 +1426,7 @@ class view {
/**
* Print table headers from child classes.
*/
- protected function print_table_headers(): void {
+ public function print_table_headers(): void {
foreach ($this->visiblecolumns as $column) {
$column->display_header();
}
@@ -1336,7 +1457,7 @@ class view {
* @param \stdClass $question
* @param int $rowcount
*/
- protected function print_table_row($question, $rowcount): void {
+ public function print_table_row($question, $rowcount): void {
$rowclasses = implode(' ', $this->get_row_classes($question, $rowcount));
$attributes = [];
if ($rowclasses) {
@@ -1381,9 +1502,30 @@ class view {
/**
* Add another search control to this view.
* @param condition $searchcondition the condition to add.
+ * @param string|null $fieldname
*/
- public function add_searchcondition($searchcondition): void {
- $this->searchconditions[] = $searchcondition;
+ public function add_searchcondition(condition $searchcondition, ?string $fieldname = null): void {
+ if (is_null($fieldname)) {
+ $this->searchconditions[] = $searchcondition;
+ } else {
+ $this->searchconditions[$fieldname] = $searchcondition;
+ }
+ }
+
+ /**
+ * Add standard search conditions.
+ * Params must be set into this object before calling this function.
+ */
+ public function add_standard_search_conditions(): void {
+ foreach ($this->plugins as $componentname => $plugin) {
+ if (\core\plugininfo\qbank::is_plugin_enabled($componentname)) {
+ $pluginentrypointobject = new $plugin();
+ $pluginobjects = $pluginentrypointobject->get_question_filters($this);
+ foreach ($pluginobjects as $pluginobject) {
+ $this->add_searchcondition($pluginobject, $pluginobject->get_condition_key());
+ }
+ }
+ }
}
/**
@@ -1411,4 +1553,23 @@ class view {
public function get_question_actions(): array {
return $this->questionactions;
}
+
+ /**
+ * Display the questions table for the fragment/ajax.
+ *
+ * @return string HTML for the question table
+ */
+ public function display_questions_table(): string {
+ $this->add_standard_search_conditions();
+ $questions = $this->load_questions();
+ $totalquestions = $this->get_question_count();
+ $questionhtml = '';
+ if ($totalquestions > 0) {
+ $this->load_required_statistics($questions);
+ ob_start();
+ $this->display_questions($questions, $this->pagevars['qpage'], $this->pagevars['qperpage']);
+ $questionhtml = ob_get_clean();
+ }
+ return $questionhtml;
+ }
}
diff --git a/question/classes/output/question_bank_filter_ui.php b/question/classes/output/question_bank_filter_ui.php
new file mode 100644
index 00000000000..ad6d4fddd2f
--- /dev/null
+++ b/question/classes/output/question_bank_filter_ui.php
@@ -0,0 +1,171 @@
+.
+
+namespace core_question\output;
+
+use core\output\datafilter;
+use renderer_base;
+use stdClass;
+
+/**
+ * Class for rendering filters on the base view.
+ *
+ * @package core_question
+ * @copyright 2021 Catalyst IT Australia Pty Ltd
+ * @author Tomo Tsuyuki
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class question_bank_filter_ui extends datafilter {
+
+ /** @var array $searchconditions Searchconditions for the filter. */
+ protected $searchconditions = array();
+
+ /** @var int $perpage number of records per page. */
+ protected $perpage = 0;
+
+ /**
+ * @var string Component for calling the fragment callback.
+ */
+ protected $component;
+
+ /**
+ * @var string Fragment callback.
+ */
+ protected $callback;
+
+ /**
+ * @var string View class name.
+ */
+ protected $view;
+
+ /**
+ * @var int|null if in an activity, the cmid.
+ */
+ protected $cmid;
+
+ /**
+ * @var array Parameters for the page URL.
+ */
+ protected $pagevars;
+
+ /**
+ * @var array Additional parameters used by view classes.
+ */
+ protected $extraparams;
+
+ /**
+ * Create a new datafilter
+ *
+ * @param \context $context The context of the course being displayed
+ * @param array $searchconditions Array of condition objects for the current filters
+ * @param array $additionalparams Additional display parameters
+ * @param string $component the component for the fragment
+ * @param string $callback the callback for the fragment
+ * @param string $view the view class
+ * @param ?string $tableregionid The ID of the region to update with the fragment
+ * @param ?int $cmid if in an activity, the cmid.
+ * @param array $pagevars current filter parameters
+ * @param array $extraparams additional parameters required for a particular view class.
+ */
+ public function __construct(
+ \context $context,
+ array $searchconditions,
+ array $additionalparams,
+ string $component,
+ string $callback,
+ string $view,
+ ?string $tableregionid = null,
+ ?int $cmid = null,
+ array $pagevars = [],
+ array $extraparams = []
+ ) {
+ parent::__construct($context, $tableregionid);
+ $this->searchconditions = $searchconditions;
+ $this->additionalparams = $additionalparams;
+ $this->component = $component;
+ $this->callback = $callback;
+ $this->view = $view;
+ $this->cmid = $cmid;
+ $this->extraparams = $extraparams;
+ if (array_key_exists('sortData', $pagevars)) {
+ foreach ($pagevars['sortData'] as $sortname => $sortorder) {
+ unset($pagevars['sortData'][$sortname]);
+ $pagevars['sortData'][str_replace('\\', '\\\\', $sortname)] = $sortorder;
+ }
+ }
+ $this->pagevars = $pagevars;
+ }
+
+ /**
+ * Get data for all filter types.
+ *
+ * @return array
+ */
+ protected function get_filtertypes(): array {
+
+ $filtertypes = [];
+
+ foreach ($this->searchconditions as $searchcondition) {
+ $filtertypes[] = $this->get_filter_object(
+ $searchcondition->get_condition_key(),
+ $searchcondition->get_title(),
+ $searchcondition->allow_custom(),
+ $searchcondition->allow_multiple(),
+ $searchcondition->get_filter_class(),
+ $searchcondition->get_initial_values(),
+ $searchcondition->allow_empty(),
+ $searchcondition->get_filteroptions(),
+ $searchcondition->is_required(),
+ $searchcondition->get_join_list(),
+ );
+ }
+
+ return $filtertypes;
+ }
+
+ /**
+ * Export the renderer data in a mustache template friendly format.
+ *
+ * @param renderer_base $output Unused.
+ * @return stdClass Data in a format compatible with a mustache template.
+ */
+ public function export_for_template(renderer_base $output): stdClass {
+ $defaultcategory = question_get_default_category($this->context->id);
+ $courseid = null;
+ if ($this->context->contextlevel == CONTEXT_COURSE) {
+ $courseid = $this->context->instanceid;
+ }
+ if (empty($courseid)) {
+ $courseid = $this->searchconditions['category']->get_course_id();
+ }
+ return (object) [
+ 'tableregionid' => $this->tableregionid,
+ 'courseid' => $courseid,
+ 'filtertypes' => $this->get_filtertypes(),
+ 'selected' => 'category',
+ 'rownumber' => 1,
+ 'categoryid' => $defaultcategory->id,
+ 'perpage' => $this->additionalparams['perpage'] ?? 0,
+ 'contextid' => $this->context->id,
+ 'component' => $this->component,
+ 'callback' => $this->callback,
+ 'view' => str_replace('\\', '\\\\', $this->view),
+ 'cmid' => $this->cmid ?? 0,
+ 'pagevars' => json_encode($this->pagevars),
+ 'extraparams' => json_encode($this->extraparams),
+ ];
+ }
+}
diff --git a/question/classes/question_reference_manager.php b/question/classes/question_reference_manager.php
index ef1d3c69896..1ccdf5f2af9 100644
--- a/question/classes/question_reference_manager.php
+++ b/question/classes/question_reference_manager.php
@@ -74,4 +74,52 @@ class question_reference_manager {
WHERE qv.questionid $qidtest
", array_merge($params, $lparams, ['draft' => question_version_status::QUESTION_STATUS_DRAFT]));
}
+
+ /**
+ * This will transform set reference filter conditions to use the new filter structure.
+ *
+ * Previously filterconditions had questioncategoryid, includesubcategories and tags options.
+ * These have been replaced by the new category and tags filters. This function convers the old
+ * pre-4.3 filter condition structure to the new one.
+ *
+ * @param array $filtercondition Pre-4.3 filter condition.
+ * @return array Post-4.3 filter condition.
+ */
+ public static function convert_legacy_set_reference_filter_condition(array $filtercondition): array {
+ if (!isset($filtercondition['filter'])) {
+ $filtercondition['filter'] = [];
+
+ // Question category filter.
+ if (isset($filtercondition['questioncategoryid'])) {
+ $filtercondition['filter']['category'] = [
+ 'jointype' => \qbank_managecategories\category_condition::JOINTYPE_DEFAULT,
+ 'values' => [$filtercondition['questioncategoryid']],
+ 'includesubcategories' => $filtercondition['includingsubcategories'],
+ ];
+ unset($filtercondition['questioncategoryid']);
+ unset($filtercondition['includingsubcategories']);
+ }
+
+ // Tag filters.
+ if (isset($filtercondition['tags'])) {
+ // Get the names of the tags in the condition. Find or create corresponding tags,
+ // and set their ids in the new condition.
+ $oldtags = array_map(fn($oldtag) => explode(',', $oldtag)[1], $filtercondition['tags']);
+ $newtags = \core_tag_tag::create_if_missing(1, $oldtags);
+ $newtagids = array_map(fn($newtag) => $newtag->id, $newtags);
+
+ $filtercondition['filter']['qtagids'] = [
+ 'jointype' => \qbank_tagquestion\tag_condition::JOINTYPE_DEFAULT,
+ 'values' => array_values($newtagids),
+ ];
+ unset($filtercondition['tags']);
+ }
+ // Add additional default properties to the filtercondition.
+ $filtercondition['tabname'] = 'questions';
+ $filtercondition['qpage'] = 0;
+ $filtercondition['qperpage'] = 100;
+ $filtercondition['jointype'] = \core\output\datafilter::JOINTYPE_ALL;
+ }
+ return $filtercondition;
+ }
}
diff --git a/question/edit.php b/question/edit.php
index dc1b6d21b6d..783fa144332 100644
--- a/question/edit.php
+++ b/question/edit.php
@@ -23,12 +23,17 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
+use core\event\question_category_viewed;
+use core_question\output\qbank_action_menu;
+use core_question\local\bank\view;
+
require_once(__DIR__ . '/../config.php');
require_once($CFG->dirroot . '/question/editlib.php');
list($thispageurl, $contexts, $cmid, $cm, $module, $pagevars) =
question_edit_setup('questions', '/question/edit.php');
+$actionurl = new moodle_url($thispageurl);
if (($lastchanged = optional_param('lastchanged', 0, PARAM_INT)) !== 0) {
$thispageurl->param('lastchanged', $lastchanged);
}
@@ -39,7 +44,7 @@ if ($PAGE->course->id == $SITE->id) {
}
$thispageurl->param('deleteall', 1);
-$questionbank = new core_question\local\bank\view($contexts, $thispageurl, $COURSE, $cm);
+$questionbank = new view($contexts, $thispageurl, $COURSE, $cm, $pagevars);
$context = $contexts->lowest();
$streditingquestions = get_string('editquestions', 'question');
@@ -53,18 +58,18 @@ echo $OUTPUT->header();
$renderer = $PAGE->get_renderer('core_question', 'bank');
// Render the selection action.
-$qbankaction = new \core_question\output\qbank_action_menu($thispageurl);
+$qbankaction = new qbank_action_menu($actionurl);
echo $renderer->render($qbankaction);
// Print the question area.
-$questionbank->display($pagevars, 'questions');
+$questionbank->display();
// Log the view of this category.
list($categoryid, $contextid) = explode(',', $pagevars['cat']);
$category = new stdClass();
$category->id = $categoryid;
-$catcontext = \context::instance_by_id($contextid);
-$event = \core\event\question_category_viewed::create_from_question_category_instance($category, $catcontext);
+$catcontext = context::instance_by_id($contextid);
+$event = question_category_viewed::create_from_question_category_instance($category, $catcontext);
$event->trigger();
echo $OUTPUT->footer();
diff --git a/question/editlib.php b/question/editlib.php
index 1d24c66f8b6..fec2787f91a 100644
--- a/question/editlib.php
+++ b/question/editlib.php
@@ -23,8 +23,7 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-
-use core_question\bank\search\category_condition;
+use core\output\datafilter;
defined('MOODLE_INTERNAL') || die();
@@ -196,23 +195,11 @@ function question_edit_setup($edittab, $baseurl, $requirecmid = false, $unused =
$params['category'] = optional_param('category', null, PARAM_SEQUENCE);
$params['qperpage'] = optional_param('qperpage', null, PARAM_INT);
- // Question table sorting options.
- for ($i = 1; $i <= core_question\local\bank\view::MAX_SORTS; $i++) {
- $param = 'qbs' . $i;
- if ($sort = optional_param($param, '', PARAM_TEXT)) {
- $params[$param] = $sort;
- } else {
- break;
- }
- }
-
// Display options.
- $params['recurse'] = optional_param('recurse', null, PARAM_BOOL);
- $params['showhidden'] = optional_param('showhidden', null, PARAM_BOOL);
- $params['qbshowtext'] = optional_param('qbshowtext', null, PARAM_INT);
+ $params['filter'] = optional_param('filter', null, PARAM_RAW);
+
// Category list page.
$params['cpage'] = optional_param('cpage', null, PARAM_INT);
- $params['qtagids'] = optional_param_array('qtagids', null, PARAM_INT);
$PAGE->set_pagelayout('admin');
@@ -235,9 +222,6 @@ function question_edit_setup($edittab, $baseurl, $requirecmid = false, $unused =
* 'cat' => PARAM_SEQUENCE,
* 'category' => PARAM_SEQUENCE,
* 'qperpage' => PARAM_INT,
- * 'recurse' => PARAM_INT,
- * 'showhidden' => PARAM_INT,
- * 'qbshowtext' => PARAM_INT,
* 'cpage' => PARAM_INT,
* 'recurse' => PARAM_BOOL,
* 'showhidden' => PARAM_BOOL,
@@ -264,7 +248,7 @@ function question_build_edit_resources($edittab, $baseurl, $params,
$cleanparams = [
'qsorts' => [],
- 'qtagids' => []
+ 'filter' => null
];
$paramtypes = [
'cmid' => PARAM_INT,
@@ -273,12 +257,7 @@ function question_build_edit_resources($edittab, $baseurl, $params,
'cat' => PARAM_SEQUENCE,
'category' => PARAM_SEQUENCE,
'qperpage' => PARAM_INT,
- 'recurse' => PARAM_INT,
- 'showhidden' => PARAM_INT,
- 'qbshowtext' => PARAM_INT,
'cpage' => PARAM_INT,
- 'recurse' => PARAM_BOOL,
- 'showhidden' => PARAM_BOOL,
'qbshowtext' => PARAM_INT,
];
@@ -290,8 +269,11 @@ function question_build_edit_resources($edittab, $baseurl, $params,
}
}
- if (!empty($params['qtagids'])) {
- $cleanparams['qtagids'] = clean_param_array($params['qtagids'], PARAM_INT);
+ if (!empty($params['filter'])) {
+ if (!is_array($params['filter'])) {
+ $params['filter'] = json_decode($params['filter'], true);
+ }
+ $cleanparams['filter'] = $params['filter'];
}
$cmid = $cleanparams['cmid'];
@@ -300,15 +282,8 @@ function question_build_edit_resources($edittab, $baseurl, $params,
$cat = $cleanparams['cat'] ?: 0;
$category = $cleanparams['category'] ?: 0;
$qperpage = $cleanparams['qperpage'];
- $recurse = $cleanparams['recurse'];
- $showhidden = $cleanparams['showhidden'];
- $qbshowtext = $cleanparams['qbshowtext'];
$cpage = $cleanparams['cpage'] ?: 1;
- $recurse = $cleanparams['recurse'];
- $showhidden = $cleanparams['showhidden'];
- $qbshowtext = $cleanparams['qbshowtext'];
$qsorts = $cleanparams['qsorts'];
- $qtagids = $cleanparams['qtagids'];
if (is_null($cmid) && is_null($courseid)) {
throw new \moodle_exception('Must provide a cmid or courseid');
@@ -399,21 +374,26 @@ function question_build_edit_resources($edittab, $baseurl, $params,
$pagevars['cat'] = "{$category->id},{$category->contextid}";
}
- // Display options.
- $pagevars['recurse'] = question_set_or_get_user_preference('recurse', $recurse, 1, $thispageurl);
- $pagevars['showhidden'] = question_set_or_get_user_preference('showhidden', $showhidden, 0, $thispageurl);
- $pagevars['qbshowtext'] = question_set_or_get_user_preference('qbshowtext', $qbshowtext, 0, $thispageurl);
-
// Category list page.
$pagevars['cpage'] = $cpage;
if ($pagevars['cpage'] != 1){
$thispageurl->param('cpage', $pagevars['cpage']);
}
- $pagevars['qtagids'] = $qtagids;
- foreach ($pagevars['qtagids'] as $index => $qtagid) {
- $thispageurl->param("qtagids[{$index}]", $qtagid);
+ if ($cleanparams['filter']) {
+ $pagevars['filter'] = $cleanparams['filter'];
+ $thispageurl->param('filter', json_encode($cleanparams['filter']));
}
+ $pagevars['tabname'] = $edittab;
+
+ // Sort parameters.
+ $pagevars['sortdata'] = optional_param_array('sortdata', [], PARAM_INT);
+ foreach ($pagevars['sortdata'] as $sortname => $sortorder) {
+ $thispageurl->param('sortdata[' . $sortname . ']', $sortorder);
+ }
+
+ // Enforce ALL as the only allowed top-level join type, so we can't bypass filtering by category.
+ $pagevars['jointype'] = datafilter::JOINTYPE_ALL;
return array($thispageurl, $contexts, $cmid, $cm, $module, $pagevars);
}
diff --git a/question/engine/bank.php b/question/engine/bank.php
index b011a678025..8605aabe1af 100644
--- a/question/engine/bank.php
+++ b/question/engine/bank.php
@@ -27,6 +27,7 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
+use core_question\local\bank\question_version_status;
use core_question\output\question_version_info;
defined('MOODLE_INTERNAL') || die();
@@ -547,7 +548,7 @@ class question_finder implements cache_data_source {
if ($extraconditions) {
$extraconditions = ' AND (' . $extraconditions . ')';
}
- $qcparams['readystatus'] = \core_question\local\bank\question_version_status::QUESTION_STATUS_READY;
+ $qcparams['readystatus'] = question_version_status::QUESTION_STATUS_READY;
$sql = "SELECT q.id, q.id AS id2
FROM {question} q
JOIN {question_versions} qv ON qv.questionid = q.id
@@ -572,9 +573,16 @@ class question_finder implements cache_data_source {
* the where clause. Must use named parameters.
* @param array $extraparams any parameters used by $extraconditions.
* @return array questionid => count of number of previous uses.
+ *
+ * @deprecated since Moodle 4.3
+ * @todo Final deprecation on Moodle 4.7 MDL-78091
*/
public function get_questions_from_categories_with_usage_counts($categoryids,
qubaid_condition $qubaids, $extraconditions = '', $extraparams = array()) {
+ debugging(
+ 'Function get_questions_from_categories_with_usage_counts() is deprecated, please do not use the function.',
+ DEBUG_DEVELOPER
+ );
return $this->get_questions_from_categories_and_tags_with_usage_counts(
$categoryids, $qubaids, $extraconditions, $extraparams);
}
@@ -592,14 +600,20 @@ class question_finder implements cache_data_source {
* @param array $extraparams any parameters used by $extraconditions.
* @param array $tagids an array of tag ids
* @return array questionid => count of number of previous uses.
+ * @deprecated since Moodle 4.3
+ * @todo Final deprecation on Moodle 4.7 MDL-78091
*/
public function get_questions_from_categories_and_tags_with_usage_counts($categoryids,
qubaid_condition $qubaids, $extraconditions = '', $extraparams = array(), $tagids = array()) {
+ debugging(
+ 'Function get_questions_from_categories_and_tags_with_usage_counts() is deprecated, please do not use the function.',
+ DEBUG_DEVELOPER
+ );
global $DB;
list($qcsql, $qcparams) = $DB->get_in_or_equal($categoryids, SQL_PARAMS_NAMED, 'qc');
- $readystatus = \core_question\local\bank\question_version_status::QUESTION_STATUS_READY;
+ $readystatus = question_version_status::QUESTION_STATUS_READY;
$select = "q.id, (SELECT COUNT(1)
FROM " . $qubaids->from_question_attempts('qa') . "
WHERE qa.questionid = q.id AND " . $qubaids->where() . "
diff --git a/question/engine/tests/helpers.php b/question/engine/tests/helpers.php
index c7da45ae584..3138de059e2 100644
--- a/question/engine/tests/helpers.php
+++ b/question/engine/tests/helpers.php
@@ -1391,3 +1391,35 @@ class question_test_recordset extends moodle_recordset {
$this->records = null;
}
}
+
+/**
+ * Provide utility function for random question test
+ *
+ * @package core_question
+ * @author Nathan Nguyen
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class question_filter_test_helper {
+ /**
+ * Create filters base on provided values
+ *
+ * @param array $categoryids question category filter
+ * @param bool $recursive subcategories filter
+ * @param array $qtagids tags filter
+ * @return array
+ */
+ public static function create_filters(array $categoryids, bool $recursive = false, array $qtagids = []): array {
+ $filters = [
+ 'category' => [
+ 'jointype' => \qbank_managecategories\category_condition::JOINTYPE_DEFAULT,
+ 'values' => $categoryids,
+ 'filteroptions' => ['includesubcategories' => $recursive],
+ ],
+ 'qtagids' => [
+ 'jointype' => \qbank_tagquestion\tag_condition::JOINTYPE_DEFAULT,
+ 'values' => $qtagids,
+ ],
+ ];
+ return $filters;
+ }
+}
diff --git a/question/engine/tests/questionbank_test.php b/question/engine/tests/questionbank_test.php
index a81aac2a543..dea9e7f704a 100644
--- a/question/engine/tests/questionbank_test.php
+++ b/question/engine/tests/questionbank_test.php
@@ -82,33 +82,6 @@ class questionbank_test extends \advanced_testcase {
$this->assertSame('-0.8333333', key($fractions));
}
- public function test_get_questions_from_categories_with_usage_counts() {
- $this->resetAfterTest();
- $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
-
- $cat = $generator->create_question_category();
- $questiondata1 = $generator->create_question('shortanswer', null, array('category' => $cat->id));
- $questiondata2 = $generator->create_question('shortanswer', null, array('category' => $cat->id));
- $questiondata3 = $generator->create_question('shortanswer', null, array('category' => $cat->id));
-
- $quba = question_engine::make_questions_usage_by_activity('test', \context_system::instance());
- $quba->set_preferred_behaviour('deferredfeedback');
- $question1 = question_bank::load_question($questiondata1->id);
- $question3 = question_bank::load_question($questiondata3->id);
- $quba->add_question($question1);
- $quba->add_question($question1);
- $quba->add_question($question3);
- $quba->start_all_questions();
- question_engine::save_questions_usage_by_activity($quba);
-
- $this->assertEquals(array(
- $questiondata2->id => 0,
- $questiondata3->id => 1,
- $questiondata1->id => 2,
- ), question_bank::get_finder()->get_questions_from_categories_with_usage_counts(
- array($cat->id), new qubaid_list(array($quba->get_id()))));
- }
-
public function test_load_many_for_cache() {
$this->resetAfterTest();
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
diff --git a/question/lib.php b/question/lib.php
index 7104a08cad6..b10c10f48ed 100644
--- a/question/lib.php
+++ b/question/lib.php
@@ -17,7 +17,7 @@
/**
* Question related functions.
*
- * This file was created just because Fragment API expects callbacks to be defined on lib.php.
+ * This file was created just because Fragment API expects callbacks to be defined on lib.php
*
* Please, do not add new functions to this file.
*
@@ -28,6 +28,8 @@
defined('MOODLE_INTERNAL') || die();
+require_once($CFG->dirroot . '/question/editlib.php');
+
/**
* Question tags fragment callback.
*
@@ -99,3 +101,54 @@ function core_question_output_fragment_tags_form($args) {
return $mform->render();
}
}
+
+/**
+ * Question data fragment to get the question html via ajax call.
+ *
+ * @param array $args Arguments for rendering the fragment. Expected keys:
+ * * view - the view class
+ * * cmid - if in an activity, the course module ID.
+ * * filterquery - the current filters encoded as a URL parameter.
+ * * lastchanged - the ID of the last edited question.
+ * * sortdata - Array of sorted columns.
+ * * filtercondition - the current filters encoded as an object.
+ * * extraparams - additional parameters required for a particular view class.
+ *
+ * @return array|string
+ */
+function core_question_output_fragment_question_data(array $args): string {
+ global $PAGE;
+ if (empty($args)) {
+ return '';
+ }
+ [$params, $extraparams] = \core_question\local\bank\filter_condition_manager::extract_parameters_from_fragment_args($args);
+ $thiscontext = \context_course::instance($params['courseid']);
+ $contexts = new \core_question\local\bank\question_edit_contexts($thiscontext);
+ $contexts->require_one_edit_tab_cap($params['tabname']);
+ $course = get_course($params['courseid']);
+
+ $viewclass = empty($args['view']) ? \core_question\local\bank\view::class : clean_param($args['view'], PARAM_NOTAGS);
+ $cmid = clean_param($args['cmid'], PARAM_INT);
+ [, $cm] = empty($cmid) ? [null, null] : get_module_from_cmid($cmid);
+
+ $nodeparent = $PAGE->settingsnav->find('questionbank', \navigation_node::TYPE_CONTAINER);
+ $thispageurl = new \moodle_url($nodeparent->action);
+ if ($cm) {
+ $thispageurl->param('cmid', $cm->id);
+ } else {
+ $thispageurl->param('courseid', $params['courseid']);
+ }
+ if (!empty($args['filterquery'])) {
+ $thispageurl->param('filter', $args['filterquery']);
+ }
+ if (!empty($args['lastchanged'])) {
+ $thispageurl->param('lastchanged', $args['lastchanged']);
+ }
+ if (!empty($params['sortdata'])) {
+ foreach ($params['sortdata'] as $sortname => $sortorder) {
+ $thispageurl->param('sortdata[' . $sortname . ']', $sortorder);
+ }
+ }
+ $questionbank = new $viewclass($contexts, $thispageurl, $course, $cm, $params, $extraparams);
+ return $questionbank->display_questions_table();
+}
diff --git a/question/renderer.php b/question/renderer.php
index f69708c3f14..7c4343bfa00 100644
--- a/question/renderer.php
+++ b/question/renderer.php
@@ -111,9 +111,16 @@ class core_question_bank_renderer extends plugin_renderer_base {
*
* @param array $displaydata
* @return bool|string
+ * @deprecated since Moodle 4.3
+ * @todo Final deprecation on Moodle 4.7 MDL-78090
*/
public function render_category_condition($displaydata) {
- return $this->render_from_template('core_question/category_condition', $displaydata);
+ debugging(
+ 'Function render_category_condition() has been deprecated and moved to qbank_managecategories plugin, ' .
+ 'Please use qbank_managecategories\output\renderer::render_category_condition() instead.',
+ DEBUG_DEVELOPER
+ );
+ return $this->render_from_template('qbank_managecategories/category_condition', $displaydata);
}
/**
@@ -121,9 +128,16 @@ class core_question_bank_renderer extends plugin_renderer_base {
*
* @param array $displaydata
* @return bool|string
+ * @deprecated since Moodle 4.3
+ * @todo Final deprecation on Moodle 4.7 MDL-78090
*/
public function render_category_condition_advanced($displaydata) {
- return $this->render_from_template('core_question/category_condition_advanced', $displaydata);
+ debugging(
+ 'Function render_category_condition_advanced() has been deprecated',
+ DEBUG_DEVELOPER
+ );
+ // The template category_condition_advanced should also be deleted with this function.
+ return $this->render_from_template('qbank_managecategories/category_condition_advanced', $displaydata);
}
/**
@@ -131,9 +145,17 @@ class core_question_bank_renderer extends plugin_renderer_base {
*
* @param array $displaydata
* @return bool|string
+ * @see qbank_deletequestion\output\renderer
+ * @deprecated since Moodle 4.3
+ * @todo Final deprecation on Moodle 4.7 MDL-78090
*/
public function render_hidden_condition_advanced($displaydata) {
- return $this->render_from_template('core_question/hidden_condition_advanced', $displaydata);
+ debugging(
+ 'Function render_hidden_condition_advanced() has been deprecated and moved to qbank_deletequestion plugin, ' .
+ 'Please use qbank_deletequestion\output\renderer::render_hidden_condition_advanced() instead.',
+ DEBUG_DEVELOPER
+ );
+ return $this->render_from_template('qbank_deletequestion/hidden_condition_advanced', $displaydata);
}
/**
diff --git a/question/templates/column_sort.mustache b/question/templates/column_sort.mustache
index 7d13f0cc372..bde218ef653 100644
--- a/question/templates/column_sort.mustache
+++ b/question/templates/column_sort.mustache
@@ -22,13 +22,15 @@
"sortdata": [
{
"sorturl": "url",
+ "sortname": "Sort name",
+ "sortorder": "Sort order when activate this sort",
"sorttip": "Sort according to first name",
"sortcontent": "Sort title sort icon"
}
]
}
}}
-
+
{{{sortcontent}}}
diff --git a/question/templates/question_bank_filter_ui.mustache b/question/templates/question_bank_filter_ui.mustache
new file mode 100644
index 00000000000..0efd86f8686
--- /dev/null
+++ b/question/templates/question_bank_filter_ui.mustache
@@ -0,0 +1,45 @@
+{{!
+ 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 .
+}}
+{{!
+ @template core_question/qbank_filter
+
+ Template for the form containing one or more filter rows.
+
+ Example context (json):
+ {
+ "tableregionid": "qbank-table",
+ "courseid": "1",
+ "categoryid": "2",
+ "perpage": "100",
+ "contextid": "3",
+ "component": "core_quiz",
+ "callback": "get_question_data",
+ "view": "\\\\core\\\\question\\\\local\\\\bank\\\\view",
+ "cmid": "4",
+ "pagevars": "[]",
+ "extraparams": "[]"
+ }
+}}
+
+{{> core/datafilter/filter}}
+
+{{#js}}
+ require(['core_question/filter'], function(Filter) {
+ Filter.init('core-filter-{{uniqid}}', {{courseid}}, {{categoryid}}, {{perpage}}, {{contextid}},
+ '{{component}}', '{{callback}}', '{{view}}', {{cmid}}, '{{{pagevars}}}', '{{{extraparams}}}');
+ });
+{{/js}}
diff --git a/question/tests/bank_view_test.php b/question/tests/bank_view_test.php
deleted file mode 100644
index b52424e8988..00000000000
--- a/question/tests/bank_view_test.php
+++ /dev/null
@@ -1,119 +0,0 @@
-.
-
-namespace core_question;
-
-use core_question\local\bank\question_edit_contexts;
-use core_question\local\bank\view;
-
-defined('MOODLE_INTERNAL') || die();
-
-global $CFG;
-require_once($CFG->dirroot . '/question/editlib.php');
-
-/**
- * Unit tests for the question bank view class.
- *
- * @package core_question
- * @category test
- * @copyright 2018 the Open University
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class bank_view_test extends \advanced_testcase {
-
- public function test_viewing_question_bank_should_not_load_individual_questions() {
- $this->resetAfterTest();
- $this->setAdminUser();
- $generator = $this->getDataGenerator();
- /** @var core_question_generator $questiongenerator */
- $questiongenerator = $generator->get_plugin_generator('core_question');
-
- // Create a course.
- $course = $generator->create_course();
- $context = \context_course::instance($course->id);
-
- // Create a question in the default category.
- $contexts = new question_edit_contexts($context);
- $cat = question_make_default_categories($contexts->all());
- $questiondata = $questiongenerator->create_question('numerical', null,
- ['name' => 'Example question', 'category' => $cat->id]);
-
- // Ensure the question is not in the cache.
- $cache = \cache::make('core', 'questiondata');
- $cache->delete($questiondata->id);
-
- // Generate the view.
- $view = new view($contexts, new \moodle_url('/'), $course);
- ob_start();
- $pagevars = [
- 'qpage' => 0,
- 'qperpage' => DEFAULT_QUESTIONS_PER_PAGE,
- 'cat' => $cat->id . ',' . $context->id,
- 'recurse' => false,
- 'showhidden' => false,
- 'qbshowtext' => false
-
- ];
- $view->display($pagevars, 'editq');
- $html = ob_get_clean();
-
- // Verify the output includes the expected question.
- $this->assertStringContainsString('Example question', $html);
-
- // Verify the question has not been loaded into the cache.
- $this->assertFalse($cache->has($questiondata->id));
- }
-
- public function test_unknown_qtype_does_not_break_view() {
- global $DB;
- $this->resetAfterTest();
- $this->setAdminUser();
- $generator = $this->getDataGenerator();
- /** @var core_question_generator $questiongenerator */
- $questiongenerator = $generator->get_plugin_generator('core_question');
-
- // Create a course.
- $course = $generator->create_course();
- $context = \context_course::instance($course->id);
-
- // Create a question in the default category.
- $contexts = new question_edit_contexts($context);
- $cat = question_make_default_categories($contexts->all());
- $questiondata = $questiongenerator->create_question('numerical', null,
- ['name' => 'Example question', 'category' => $cat->id]);
- $DB->set_field('question', 'qtype', 'unknownqtype', ['id' => $questiondata->id]);
-
- // Generate the view.
- $view = new view($contexts, new \moodle_url('/'), $course);
- ob_start();
- $pagevars = [
- 'qpage' => 0,
- 'qperpage' => DEFAULT_QUESTIONS_PER_PAGE,
- 'cat' => $cat->id . ',' . $context->id,
- 'recurse' => false,
- 'showhidden' => false,
- 'qbshowtext' => false
-
- ];
- $view->display($pagevars, 'editq');
- $html = ob_get_clean();
-
- // Mainly we are verifying that there was no fatal error.
-
- // Verify the output includes the expected question.
- $this->assertStringContainsString('Example question', $html);
- }
-}
diff --git a/question/tests/behat/bank_view.feature b/question/tests/behat/bank_view.feature
new file mode 100644
index 00000000000..37fdd59e3e2
--- /dev/null
+++ b/question/tests/behat/bank_view.feature
@@ -0,0 +1,48 @@
+@core @core_question
+Feature: A bank view with questions can be managed
+ In order to manage a question bank from the course
+ As a teacher
+ I need to be able to view and manage questions
+
+ 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 | name |
+ | Course | C1 | Test questions |
+
+ @javascript
+ Scenario: Viewing question bank should not load individual questions
+ When the following "questions" exist:
+ | questioncategory | qtype | name | questiontext | idnumber |
+ | Test questions | essay | Essay test question | Write about whatever you want | qid |
+ | Test questions | numerical | Numerical test question | Write about whatever you want | qid |
+ And I am on the "C1" "Course" page logged in as "teacher1"
+ And I navigate to "Question bank" in current page administration
+ And I should see "Essay test question"
+ And I should see "Numerical test question"
+ And I choose "Delete" action for "Essay test question" in the question bank
+ And I press "Delete"
+ And I should not see "Essay test question"
+ And I choose "Delete" action for "Numerical test question" in the question bank
+ And I press "Delete"
+
+ @javascript
+ Scenario: Unknown qtype does not break the view
+ When the following "questions" exist:
+ | questioncategory | qtype | name | questiontext |
+ | Test questions | missingtype | Unknown type question | Write about whatever you want |
+ | Test questions | truefalse | Truefalse type question | Write about whatever you want |
+ | Test questions | essay | Essay type question | Write about whatever you want |
+ And I am on the "C1" "Course" page logged in as "teacher1"
+ And I navigate to "Question bank" in current page administration
+ And I should see "Unknown type question"
+ And I should see "Truefalse type question"
+ And I should see "Essay type question"
diff --git a/question/tests/behat/behat_core_question.php b/question/tests/behat/behat_core_question.php
index b9252e2e56e..8bf8eb90c03 100644
--- a/question/tests/behat/behat_core_question.php
+++ b/question/tests/behat/behat_core_question.php
@@ -299,4 +299,53 @@ class behat_core_question extends behat_question_base {
$DB->delete_records('question', ['id' => $id]);
question_bank::notify_question_edited($id);
}
+
+ /**
+ * Add a question bank filter
+ *
+ * This will add the filter if it does not exist, but leave the value empty.
+ *
+ * @When I add question bank filter :filtertype
+ * @param string $filtertype The filter we are adding
+ */
+ public function i_add_question_bank_filter(string $filtertype) {
+ $filter = $this->getSession()->getPage()->find('css',
+ '[data-filterregion=filter] [data-field-title="' . $filtertype . '"]');
+ if ($filter === null) {
+ $this->execute('behat_forms::press_button', [get_string('addcondition')]);
+ $this->execute('behat_forms::i_set_the_field_in_container_to', [
+ "type",
+ "[data-filterregion=filter]:last-child fieldset",
+ "css_element",
+ $filtertype
+ ]);
+ }
+ }
+
+ /**
+ * Apply question bank filter.
+ *
+ * This will change the existing value of the specified filter, or add the filter and set its value if it doesn't already
+ * exist.
+ *
+ * @When I apply question bank filter :filtertype with value :value
+ * @param string $filtertype The filter to apply. This should match the get_title() return value from the
+ * filter's condition class.
+ * @param string $value The value to set for the condition.
+ */
+ public function i_apply_question_bank_filter(string $filtertype, string $value) {
+ // Add the filter if needed.
+ $this->execute('behat_core_question::i_add_question_bank_filter', [
+ $filtertype,
+ ]);
+
+ // Set the filter value.
+ $this->execute('behat_forms::i_set_the_field_to', [
+ $filtertype,
+ $value
+ ]);
+
+ // Apply filters.
+ $this->execute("behat_forms::press_button", [get_string('applyfilters')]);
+ }
}
diff --git a/question/tests/behat/delete_questions.feature b/question/tests/behat/delete_questions.feature
index 4048d92b583..1800c07936c 100644
--- a/question/tests/behat/delete_questions.feature
+++ b/question/tests/behat/delete_questions.feature
@@ -26,7 +26,7 @@ Feature: A teacher can delete questions in the question bank
Scenario: A question not used anywhere can really be deleted
When I choose "Delete" action for "Test question to be deleted" in the question bank
And I press "Delete"
- And I set the field "Also show old questions" to "1"
+ And I apply question bank filter "Show hidden questions" with value "Yes"
Then I should not see "Test question to be deleted"
Scenario: Deleting a question can be cancelled
@@ -55,7 +55,7 @@ Feature: A teacher can delete questions in the question bank
And I should see "* Denotes questions which can't be deleted because they are in use. Instead, they will be hidden in the question bank unless you select 'Show old questions'."
And I press "Delete"
Then I should not see "Test used question to be deleted"
- And I set the field "Also show old questions" to "1"
+ And I apply question bank filter "Show hidden questions" with value "Yes"
And I should see "Test used question to be deleted"
And I am on the "Test quiz" "quiz activity" page
And I click on "Preview quiz" "button"
@@ -69,7 +69,7 @@ Feature: A teacher can delete questions in the question bank
And I reload the page
When I choose "Delete" action for "Broken question" in the question bank
And I press "Delete"
- And I set the field "Also show old questions" to "1"
+ And I apply question bank filter "Show hidden questions" with value "Yes"
Then I should not see "Broken question"
@javascript
diff --git a/question/tests/behat/duplicate_questions.feature b/question/tests/behat/duplicate_questions.feature
index 22424d2b524..0b762b548fc 100644
--- a/question/tests/behat/duplicate_questions.feature
+++ b/question/tests/behat/duplicate_questions.feature
@@ -42,7 +42,7 @@ Feature: A teacher can duplicate questions in the question bank
When I choose "Duplicate" action for "Test question to be copied" in the question bank
And I press "Cancel"
Then I should see "Test question to be copied"
- And the field "Select a category" matches value " Test questions (1)"
+ And I should see "Test questions (1)" in the "Filter 1" "fieldset"
Scenario: Duplicating a question with an idnumber increments it
Given the following "questions" exist:
diff --git a/question/tests/behat/filter_questions_by_tag.feature b/question/tests/behat/filter_questions_by_tag.feature
index 6cfdc612d11..4432999b305 100644
--- a/question/tests/behat/filter_questions_by_tag.feature
+++ b/question/tests/behat/filter_questions_by_tag.feature
@@ -32,7 +32,14 @@ Feature: The questions in the question bank can be filtered by tags
@javascript
Scenario: The questions can be filtered by tag
- When I set the field "Filter by tags..." to "foo"
- And I press the enter key
+ When I apply question bank filter "Tag" with value "foo"
Then I should see "question 1 name" in the "categoryquestions" "table"
And I should not see "question 2 name" in the "categoryquestions" "table"
+
+ @javascript
+ Scenario: Empty condition should not result in exception
+ When I am on the "Course 1" "core_question > course question bank" page
+ And I set the field "Type or select..." in the "Filter 1" "fieldset" to "Test questions"
+ When I click on "Add condition" "button"
+ And I set the field "type" in the "Filter 2" "fieldset" to "Tag"
+ And I click on "Apply filters" "button"
diff --git a/question/tests/behat/filter_questions_combined_conditions.feature b/question/tests/behat/filter_questions_combined_conditions.feature
new file mode 100644
index 00000000000..a0a4380cc1e
--- /dev/null
+++ b/question/tests/behat/filter_questions_combined_conditions.feature
@@ -0,0 +1,40 @@
+@core @core_question
+Feature: The questions in the question bank can be filtered by combine various conditions
+ In order to find the questions I need
+ As a teacher
+ I want to filter the questions by various conditions
+
+ 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 | name |
+ | Course | C1 | Test questions 1|
+ | Course | C1 | Test questions 2|
+ And the following "questions" exist:
+ | questioncategory | qtype | name | user | questiontext |
+ | Test questions 1 | essay | question 1 name | teacher1 | Question 1 text |
+ | Test questions 1 | essay | question 2 name | teacher1 | Question 2 text |
+ | Test questions 2 | essay | question 3 name | teacher1 | Question 3 text |
+ | Test questions 2 | essay | question 4 name | teacher1 | Question 4 text |
+ And the following "core_question > Tags" exist:
+ | question | tag |
+ | question 1 name | foo |
+ | question 3 name | foo |
+ And I am on the "Course 1" "core_question > course question bank" page logged in as "teacher1"
+
+ @javascript
+ Scenario: The questions can be filtered by matching all conditions
+ When I apply question bank filter "Category" with value "Test questions 1"
+ And I apply question bank filter "Tag" with value "foo"
+ Then I should see "question 1 name" in the "categoryquestions" "table"
+ And I should not see "question 2 name" in the "categoryquestions" "table"
+ And I should not see "question 3 name" in the "categoryquestions" "table"
+ And I should not see "question 4 name" in the "categoryquestions" "table"
diff --git a/question/tests/behat/question_categories.feature b/question/tests/behat/question_categories.feature
index e449d86a945..ba147b8f1fb 100644
--- a/question/tests/behat/question_categories.feature
+++ b/question/tests/behat/question_categories.feature
@@ -29,13 +29,11 @@ Feature: A teacher can move questions between categories in the question bank
@javascript
Scenario: Move a question between categories via the question page
When I am on the "Course 1" "core_question > course question bank" page logged in as "teacher1"
- And I set the field "Select a category" to "Used category"
+ And I apply question bank filter "Category" with value "Used category"
And I click on "Test question to be moved" "checkbox" in the "Test question to be moved" "table_row"
And I click on "With selected" "button"
And I click on question bulk action "move"
And I set the field "Question category" to "Subcategory"
And I press "Move to"
Then I should see "Test question to be moved"
- And the field "Select a category" matches value " Subcategory (1)"
- And the "Select a category" select box should contain "Used category"
- And the "Select a category" select box should not contain "Used category (1)"
+ And I should see "Subcategory (1)" in the ".form-autocomplete-selection" "css_element"
diff --git a/question/tests/behat/question_filter_pagination.feature b/question/tests/behat/question_filter_pagination.feature
new file mode 100644
index 00000000000..3af96c874eb
--- /dev/null
+++ b/question/tests/behat/question_filter_pagination.feature
@@ -0,0 +1,38 @@
+@core @core_question @qbank_filter @javascript
+Feature: A teacher can pagimate through question bank questions
+ In order to paginate questions
+ As a teacher
+ I must be able to paginate
+
+ 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 | Used category |
+ Given 100 "questions" exist with the following data:
+ | questioncategory | Used category |
+ | qtype | essay |
+ | name | Tests question [count] |
+ | questiontext | Write about whatever you want |
+ And the following "questions" exist:
+ | questioncategory | qtype | name | questiontext |
+ | Used category | essay | Not on first page | Write about whatever you want |
+
+ Scenario: Questions can be paginated
+ When I am on the "Course 1" "core_question > course question bank" page logged in as "teacher1"
+ When I apply question bank filter "Category" with value "Course 1"
+ And I follow "Sort by Question name ascending"
+ And I follow "Sort by Question name descending"
+ And I should see "Tests question 1"
+ And I should not see "Not on first page"
+ And I click on "2" "link" in the ".pagination" "css_element"
+ And I should not see "Tests question 1"
+ And I should see "Not on first page"
diff --git a/question/tests/behat/question_highlight.feature b/question/tests/behat/question_highlight.feature
index 46d67734637..6b2c74bc501 100644
--- a/question/tests/behat/question_highlight.feature
+++ b/question/tests/behat/question_highlight.feature
@@ -24,12 +24,12 @@ Feature: A teacher can see highlighted questions in the question bank
| questiontext | Write about whatever you want |
Scenario: Edited question highlight is retained when go to multiple pages.
- Given I am on the "Course 1" "core_question > course question bank" page logged in as "teacher1"
- And I am on the "essay 1" "core_question > edit" page logged in as "teacher1"
+ Given I am on the "essay 1" "core_question > edit" page logged in as "teacher1"
And I set the following fields to these values:
| Question name | essay 1 edited |
And I press "id_submitbutton"
And I should see "essay 1 edited"
And ".highlight" "css_element" should exist in the "#categoryquestions" "css_element"
- When I click on "Show all 101" "link"
+ When I click on "2" "link" in the ".pagination" "css_element"
+ And I click on "1" "link" in the ".pagination" "css_element"
Then ".highlight" "css_element" should exist in the "#categoryquestions" "css_element"
diff --git a/question/tests/behat/sort_questions.feature b/question/tests/behat/sort_questions.feature
index 98dd3da47f7..70f9a62744d 100644
--- a/question/tests/behat/sort_questions.feature
+++ b/question/tests/behat/sort_questions.feature
@@ -61,7 +61,7 @@ Feature: The questions in the question bank can be sorted in various ways
@javascript
Scenario: The question text can be shown in the list of questions
- When I set the field "Show question text in the question list?" to "1"
+ When I apply question bank filter "Show question text in the question list?" with value "Yes"
Then I should see "Question 1 text"
And I should see "Question 2 text"
And I should see "Question 3 text"
diff --git a/question/tests/local/statistics/statistics_bulk_loader_test.php b/question/tests/local/statistics/statistics_bulk_loader_test.php
index 2ce29c378b1..14c14321dba 100644
--- a/question/tests/local/statistics/statistics_bulk_loader_test.php
+++ b/question/tests/local/statistics/statistics_bulk_loader_test.php
@@ -16,6 +16,8 @@
namespace core_question\local\statistics;
+defined('MOODLE_INTERNAL') || die();
+
use advanced_testcase;
use context;
use context_module;
@@ -27,6 +29,9 @@ use mod_quiz\quiz_settings;
use question_engine;
use ReflectionMethod;
+global $CFG;
+require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
+
/**
* Tests for question statistics.
*
@@ -37,6 +42,8 @@ use ReflectionMethod;
*/
class statistics_bulk_loader_test extends advanced_testcase {
+ use \quiz_question_helper_test_trait;
+
/** @var float Delta used when comparing statistics values out-of 1. */
protected const DELTA = 0.00005;
@@ -111,7 +118,7 @@ class statistics_bulk_loader_test extends advanced_testcase {
$this->assertEquals((object) ['component' => 'mod_quiz', 'contextid' => $quiz2context->id], $q2places[0]);
// Add a random question to quiz3.
- quiz_add_random_questions($quiz3, 0, $cat->id, 1, false);
+ $this->add_random_questions($quiz3->id, 0, $cat->id, 1, false);
$this->submit_quiz($quiz3, [1 => ['answer' => 'willbewrong']]);
// Quiz 3 will now be in one of these arrays.
diff --git a/question/tests/random_question_loader_test.php b/question/tests/random_question_loader_test.php
index 8e706765ef3..13cc9dec3aa 100644
--- a/question/tests/random_question_loader_test.php
+++ b/question/tests/random_question_loader_test.php
@@ -19,9 +19,16 @@ namespace core_question;
use qubaid_list;
use question_bank;
use question_engine;
+use question_filter_test_helper;
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
+require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
/**
- * Tests for the {@see core_question\local\bank\random_question_loader} class.
+ * Tests for the {@see \core_question\local\bank\random_question_loader} class.
*
* @package core_question
* @copyright 2015 The Open University
@@ -29,6 +36,8 @@ use question_engine;
*/
class random_question_loader_test extends \advanced_testcase {
+ use \quiz_question_helper_test_trait;
+
public function test_empty_category_gives_null() {
$this->resetAfterTest();
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
@@ -36,14 +45,18 @@ class random_question_loader_test extends \advanced_testcase {
$cat = $generator->create_question_category();
$loader = new \core_question\local\bank\random_question_loader(new qubaid_list([]));
- $this->assertNull($loader->get_next_question_id($cat->id, 0));
- $this->assertNull($loader->get_next_question_id($cat->id, 1));
+ $filters = question_filter_test_helper::create_filters([$cat->id]);
+ $this->assertNull($loader->get_next_filtered_question_id($filters));
+
+ $filters = question_filter_test_helper::create_filters([$cat->id], 1);
+ $this->assertNull($loader->get_next_filtered_question_id($filters));
}
public function test_unknown_category_behaves_like_empty() {
// It is up the caller to make sure the category id is valid.
$loader = new \core_question\local\bank\random_question_loader(new qubaid_list([]));
- $this->assertNull($loader->get_next_question_id(-1, 1));
+ $filters = question_filter_test_helper::create_filters([-1], 1);
+ $this->assertNull($loader->get_next_filtered_question_id($filters));
}
public function test_descriptions_not_returned() {
@@ -54,7 +67,8 @@ class random_question_loader_test extends \advanced_testcase {
$info = $generator->create_question('description', null, ['category' => $cat->id]);
$loader = new \core_question\local\bank\random_question_loader(new qubaid_list([]));
- $this->assertNull($loader->get_next_question_id($cat->id, 0));
+ $filters = question_filter_test_helper::create_filters([$cat->id]);
+ $this->assertNull($loader->get_next_filtered_question_id($filters));
}
public function test_hidden_questions_not_returned() {
@@ -68,7 +82,8 @@ class random_question_loader_test extends \advanced_testcase {
\core_question\local\bank\question_version_status::QUESTION_STATUS_HIDDEN, ['questionid' => $question1->id]);
$loader = new \core_question\local\bank\random_question_loader(new qubaid_list([]));
- $this->assertNull($loader->get_next_question_id($cat->id, 0));
+ $filters = question_filter_test_helper::create_filters([$cat->id]);
+ $this->assertNull($loader->get_next_filtered_question_id($filters));
}
public function test_cloze_subquestions_not_returned() {
@@ -79,8 +94,9 @@ class random_question_loader_test extends \advanced_testcase {
$question1 = $generator->create_question('multianswer', null, ['category' => $cat->id]);
$loader = new \core_question\local\bank\random_question_loader(new qubaid_list([]));
- $this->assertEquals($question1->id, $loader->get_next_question_id($cat->id, 0));
- $this->assertNull($loader->get_next_question_id($cat->id, 0));
+ $filters = question_filter_test_helper::create_filters([$cat->id]);
+ $this->assertEquals($question1->id, $loader->get_next_filtered_question_id($filters));
+ $this->assertNull($loader->get_next_filtered_question_id($filters));
}
public function test_random_questions_not_returned() {
@@ -91,10 +107,11 @@ class random_question_loader_test extends \advanced_testcase {
$cat = $generator->create_question_category();
$course = $this->getDataGenerator()->create_course();
$quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course]);
- quiz_add_random_questions($quiz, 1, $cat->id, 1, false);
+ $this->add_random_questions($quiz->id, 1, $cat->id, 1);
$loader = new \core_question\local\bank\random_question_loader(new qubaid_list([]));
- $this->assertNull($loader->get_next_question_id($cat->id, 0));
+ $filters = question_filter_test_helper::create_filters([$cat->id]);
+ $this->assertNull($loader->get_next_filtered_question_id($filters));
}
public function test_one_question_category_returns_that_q_then_null() {
@@ -105,8 +122,11 @@ class random_question_loader_test extends \advanced_testcase {
$question1 = $generator->create_question('shortanswer', null, ['category' => $cat->id]);
$loader = new \core_question\local\bank\random_question_loader(new qubaid_list([]));
- $this->assertEquals($question1->id, $loader->get_next_question_id($cat->id, 1));
- $this->assertNull($loader->get_next_question_id($cat->id, 0));
+ $filters = question_filter_test_helper::create_filters([$cat->id], 1);
+ $this->assertEquals($question1->id, $loader->get_next_filtered_question_id($filters));
+
+ $filters = question_filter_test_helper::create_filters([$cat->id]);
+ $this->assertNull($loader->get_next_filtered_question_id($filters));
}
public function test_two_question_category_returns_both_then_null() {
@@ -119,12 +139,14 @@ class random_question_loader_test extends \advanced_testcase {
$loader = new \core_question\local\bank\random_question_loader(new qubaid_list([]));
$questionids = [];
- $questionids[] = $loader->get_next_question_id($cat->id, 0);
- $questionids[] = $loader->get_next_question_id($cat->id, 0);
+ $filters = question_filter_test_helper::create_filters([$cat->id]);
+ $questionids[] = $loader->get_next_filtered_question_id($filters);
+ $questionids[] = $loader->get_next_filtered_question_id($filters);
sort($questionids);
$this->assertEquals([$question1->id, $question2->id], $questionids);
- $this->assertNull($loader->get_next_question_id($cat->id, 1));
+ $filters = question_filter_test_helper::create_filters([$cat->id], 1);
+ $this->assertNull($loader->get_next_filtered_question_id($filters));
}
public function test_nested_categories() {
@@ -137,10 +159,13 @@ class random_question_loader_test extends \advanced_testcase {
$question2 = $generator->create_question('shortanswer', null, ['category' => $cat2->id]);
$loader = new \core_question\local\bank\random_question_loader(new qubaid_list([]));
- $this->assertEquals($question2->id, $loader->get_next_question_id($cat2->id, 1));
- $this->assertEquals($question1->id, $loader->get_next_question_id($cat1->id, 1));
+ $filters = question_filter_test_helper::create_filters([$cat2->id], 1);
+ $this->assertEquals($question2->id, $loader->get_next_filtered_question_id($filters));
+ $filters = question_filter_test_helper::create_filters([$cat1->id], 1);
+ $this->assertEquals($question1->id, $loader->get_next_filtered_question_id($filters));
- $this->assertNull($loader->get_next_question_id($cat1->id, 0));
+ $filters = question_filter_test_helper::create_filters([$cat1->id]);
+ $this->assertNull($loader->get_next_filtered_question_id($filters));
}
public function test_used_question_not_returned_until_later() {
@@ -153,8 +178,9 @@ class random_question_loader_test extends \advanced_testcase {
$loader = new \core_question\local\bank\random_question_loader(new qubaid_list([]),
array($question2->id => 2));
- $this->assertEquals($question1->id, $loader->get_next_question_id($cat->id, 0));
- $this->assertNull($loader->get_next_question_id($cat->id, 0));
+ $filters = question_filter_test_helper::create_filters([$cat->id]);
+ $this->assertEquals($question1->id, $loader->get_next_filtered_question_id($filters));
+ $this->assertNull($loader->get_next_filtered_question_id($filters));
}
public function test_previously_used_question_not_returned_until_later() {
@@ -174,9 +200,10 @@ class random_question_loader_test extends \advanced_testcase {
$loader = new \core_question\local\bank\random_question_loader(new qubaid_list(array($quba->get_id())));
- $this->assertEquals($question1->id, $loader->get_next_question_id($cat->id, 0));
- $this->assertEquals($question2->id, $loader->get_next_question_id($cat->id, 0));
- $this->assertNull($loader->get_next_question_id($cat->id, 0));
+ $filters = question_filter_test_helper::create_filters([$cat->id]);
+ $this->assertEquals($question1->id, $loader->get_next_filtered_question_id($filters));
+ $this->assertEquals($question2->id, $loader->get_next_filtered_question_id($filters));
+ $this->assertNull($loader->get_next_filtered_question_id($filters));
}
public function test_empty_category_does_not_have_question_available() {
@@ -186,8 +213,10 @@ class random_question_loader_test extends \advanced_testcase {
$cat = $generator->create_question_category();
$loader = new \core_question\local\bank\random_question_loader(new qubaid_list(array()));
- $this->assertFalse($loader->is_question_available($cat->id, 0, 1));
- $this->assertFalse($loader->is_question_available($cat->id, 1, 1));
+ $filters = question_filter_test_helper::create_filters([$cat->id]);
+ $this->assertFalse($loader->is_filtered_question_available($filters, 1));
+ $filters = question_filter_test_helper::create_filters([$cat->id], 1);
+ $this->assertFalse($loader->is_filtered_question_available($filters, 1));
}
public function test_descriptions_not_available() {
@@ -198,8 +227,10 @@ class random_question_loader_test extends \advanced_testcase {
$info = $generator->create_question('description', null, array('category' => $cat->id));
$loader = new \core_question\local\bank\random_question_loader(new qubaid_list(array()));
- $this->assertFalse($loader->is_question_available($cat->id, 0, $info->id));
- $this->assertFalse($loader->is_question_available($cat->id, 1, $info->id));
+ $filters = question_filter_test_helper::create_filters([$cat->id]);
+ $this->assertFalse($loader->is_filtered_question_available($filters, $info->id));
+ $filters = question_filter_test_helper::create_filters([$cat->id], 1);
+ $this->assertFalse($loader->is_filtered_question_available($filters, $info->id));
}
public function test_existing_question_is_available_but_then_marked_used() {
@@ -210,10 +241,11 @@ class random_question_loader_test extends \advanced_testcase {
$question1 = $generator->create_question('shortanswer', null, array('category' => $cat->id));
$loader = new \core_question\local\bank\random_question_loader(new qubaid_list(array()));
- $this->assertTrue($loader->is_question_available($cat->id, 0, $question1->id));
- $this->assertFalse($loader->is_question_available($cat->id, 0, $question1->id));
+ $filters = question_filter_test_helper::create_filters([$cat->id]);
+ $this->assertTrue($loader->is_filtered_question_available($filters, $question1->id));
+ $this->assertFalse($loader->is_filtered_question_available($filters, $question1->id));
- $this->assertFalse($loader->is_question_available($cat->id, 0, -1));
+ $this->assertFalse($loader->is_filtered_question_available($filters, -1));
}
/**
@@ -343,7 +375,8 @@ class random_question_loader_test extends \advanced_testcase {
}, $usetagnames);
$loader = new \core_question\local\bank\random_question_loader(new qubaid_list([]));
- $result = $loader->get_questions($category->id, $includesubcategories, $tagids);
+ $filters = question_filter_test_helper::create_filters([$category->id], $includesubcategories, $tagids);
+ $result = $loader->get_filtered_questions($filters);
// Generate the expected question set.
$expectedquestions = array_map(function($index) use ($questions) {
return $questions[$index];
@@ -375,12 +408,10 @@ class random_question_loader_test extends \advanced_testcase {
array_walk($questions, function (&$value) use (&$questionsbyid) {
$questionsbyid[$value->id] = $value;
});
-
+ $filters = question_filter_test_helper::create_filters([$category->id], $includesubcategories, $tagids);
for ($i = 0; $i < $numberofquestions; $i++) {
- $result = $loader->get_questions(
- $category->id,
- $includesubcategories,
- $tagids,
+ $result = $loader->get_filtered_questions(
+ $filters,
$limit,
$offset
);
@@ -407,10 +438,9 @@ class random_question_loader_test extends \advanced_testcase {
$loader = new \core_question\local\bank\random_question_loader(new qubaid_list([]));
list($category, $questions) = $this->create_category_and_questions(1);
- $result = $loader->get_questions(
- $category->id,
- $includesubcategories,
- $tagids,
+ $filters = question_filter_test_helper::create_filters([$category->id], $includesubcategories, $tagids);
+ $result = $loader->get_filtered_questions(
+ $filters,
$limit,
$offset,
$fields
@@ -552,8 +582,9 @@ class random_question_loader_test extends \advanced_testcase {
return $tags[$tagname]->id;
}, $usetagnames);
+ $filters = question_filter_test_helper::create_filters([$category->id], $includesubcategories, $tagids);
$loader = new \core_question\local\bank\random_question_loader(new qubaid_list([]));
- $result = $loader->count_questions($category->id, $includesubcategories, $tagids);
+ $result = $loader->count_filtered_questions($filters);
// Ensure the result matches what was expected.
$this->assertEquals($expectedcount, $result);
diff --git a/theme/boost/scss/moodle/forms.scss b/theme/boost/scss/moodle/forms.scss
index f417f2dff29..b942a74c9da 100644
--- a/theme/boost/scss/moodle/forms.scss
+++ b/theme/boost/scss/moodle/forms.scss
@@ -301,6 +301,11 @@ fieldset.coursesearchbox label {
background-color: $dropdown-link-active-bg;
color: $dropdown-link-active-color;
}
+ &[aria-disabled="true"] {
+ pointer-events: none;
+ color: $custom-select-disabled-color;
+ background-color: $custom-select-disabled-bg;
+ }
&::before {
content: "\200B";
}
diff --git a/theme/boost/scss/moodle/question.scss b/theme/boost/scss/moodle/question.scss
index 2186c7f9ab0..183bba095b5 100644
--- a/theme/boost/scss/moodle/question.scss
+++ b/theme/boost/scss/moodle/question.scss
@@ -648,6 +648,13 @@ body.path-question-type .mform fieldset.hidden {
position: relative;
}
+// qbank_managecategories
+[data-filterregion=filter][data-filter-type=category] .form-autocomplete-suggestions li[aria-disabled=true] {
+ font-weight: bold;
+ color: $body-color;
+ background-color: $body-bg;
+}
+
@include media-breakpoint-down(sm) {
.que .info {
float: none;
diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css
index 9c365f31e7d..a101190d069 100644
--- a/theme/boost/style/moodle.css
+++ b/theme/boost/style/moodle.css
@@ -32181,6 +32181,12 @@ body.path-question-type .mform fieldset.hidden {
position: relative;
}
+[data-filterregion=filter][data-filter-type=category] .form-autocomplete-suggestions li[aria-disabled=true] {
+ font-weight: bold;
+ color: #1d2125;
+ background-color: #fff;
+}
+
@media (max-width: 767.98px) {
.que .info {
float: none;
@@ -32872,6 +32878,11 @@ fieldset.coursesearchbox label {
background-color: #0f6cbf;
color: #fff;
}
+.form-autocomplete-suggestions li[aria-disabled=true] {
+ pointer-events: none;
+ color: #6a737b;
+ background-color: #e9ecef;
+}
.form-autocomplete-suggestions li::before {
content: "";
}
diff --git a/theme/classic/style/moodle.css b/theme/classic/style/moodle.css
index 5b9ab23fdc0..722339cac06 100644
--- a/theme/classic/style/moodle.css
+++ b/theme/classic/style/moodle.css
@@ -32181,6 +32181,12 @@ body.path-question-type .mform fieldset.hidden {
position: relative;
}
+[data-filterregion=filter][data-filter-type=category] .form-autocomplete-suggestions li[aria-disabled=true] {
+ font-weight: bold;
+ color: #1d2125;
+ background-color: #fff;
+}
+
@media (max-width: 767.98px) {
.que .info {
float: none;
@@ -32872,6 +32878,11 @@ fieldset.coursesearchbox label {
background-color: #0f6cbf;
color: #fff;
}
+.form-autocomplete-suggestions li[aria-disabled=true] {
+ pointer-events: none;
+ color: #6a737b;
+ background-color: #e9ecef;
+}
.form-autocomplete-suggestions li::before {
content: "";
}