MDL-72397 qbank_managecategories: UI enhancement
This implementation will introduce a new user interface for "qbank_managecategories" plugin which will allow user to drag and drop. Having this plugin will give users the flexibility of reordering, adding or editing categories from the qbank_managecategories view. Co-authored-by: Marc-Alexandre Ghaly <[email protected]> Co-authored-by: Luca Bösch <[email protected]>
This commit is contained in:
co-authored by
Marc-Alexandre Ghaly
Luca Bösch
parent
2419bd64de
commit
2dad710f12
@@ -0,0 +1,7 @@
|
||||
issueNumber: MDL-72397
|
||||
notes:
|
||||
qbank_managecategories:
|
||||
- message: >-
|
||||
question_category_list and question_category_list_item are no longer
|
||||
used, and are deprecated. Category lists are now generated by templates.
|
||||
type: deprecated
|
||||
@@ -99,22 +99,12 @@ Feature: Edit quiz page - adding things
|
||||
in various categories and add them to the question bank.
|
||||
|
||||
# Create a couple of sub categories.
|
||||
When I am on the "Course 1" "core_question > course question categories" page
|
||||
Then I should see "Add category"
|
||||
And I follow "Add category"
|
||||
Then I set the field "Parent category" to "Default for C1"
|
||||
And I set the field "Name" to "Subcat 1"
|
||||
And I set the field "Category info" to "This is sub category 1"
|
||||
And I press "id_submitbutton"
|
||||
And I should see "Subcat 1"
|
||||
|
||||
And I follow "Add category"
|
||||
Then I set the field "Parent category" to "Default for C1"
|
||||
And I set the field "Name" to "Subcat 2"
|
||||
And I set the field "Category info" to "This is sub category 2"
|
||||
And I press "id_submitbutton"
|
||||
And I should see "Subcat 2"
|
||||
|
||||
Given the following "question categories" exist:
|
||||
| contextlevel | reference | questioncategory | name |
|
||||
| Course | C1 | Default for C1 | Subcat 1 |
|
||||
| Course | C1 | Default for C1 | Subcat 2 |
|
||||
When I am on "Course 1" course homepage
|
||||
And I navigate to "Question bank" in current page administration
|
||||
And I select "Questions" from the "Question bank tertiary navigation" singleselect
|
||||
And I should see "Question bank"
|
||||
|
||||
|
||||
@@ -62,11 +62,13 @@ Feature: Moving a question to another category should not affect random question
|
||||
And I press "Add random question"
|
||||
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 open the action menu in "Used category" "list_item"
|
||||
And I choose "Edit" in the open action menu
|
||||
And I set the following fields to these values:
|
||||
| Name | Used category new |
|
||||
| Category info | I was edited |
|
||||
And I press "Save changes"
|
||||
And I set the field "Show descriptions" to "1"
|
||||
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
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
define("qbank_managecategories/addcategory",["exports","core/reactive","qbank_managecategories/categorymanager"],(function(_exports,_reactive,_categorymanager){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;class _default extends _reactive.BaseComponent{create(descriptor){this.name=descriptor.element.id,this.selectors={ADD_BUTTON:'[data-action="addeditcategory"]'}}stateReady(){this.addEventListener(this.getElement(this.selectors.ADD_BUTTON),"click",_categorymanager.categorymanager.showEditModal)}static init(target,selectors){return new this({element:document.querySelector(target),selectors:selectors,reactive:_categorymanager.categorymanager})}}return _exports.default=_default,_exports.default}));
|
||||
|
||||
//# sourceMappingURL=addcategory.min.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"addcategory.min.js","sources":["../src/addcategory.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 <http://www.gnu.org/licenses/>.\n\n/**\n * Add category button for displaying the modal form.\n *\n * This just connects up the button to the showEditModal listener.\n *\n * @module qbank_managecategories/addcategory\n * @class qbank_managecategories/addcategory\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {categorymanager} from 'qbank_managecategories/categorymanager';\n\nexport default class extends BaseComponent {\n\n create(descriptor) {\n this.name = descriptor.element.id;\n this.selectors = {\n ADD_BUTTON: '[data-action=\"addeditcategory\"]',\n };\n }\n\n stateReady() {\n this.addEventListener(this.getElement(this.selectors.ADD_BUTTON), 'click', categorymanager.showEditModal);\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n const targetElement = document.querySelector(target);\n return new this({\n element: targetElement,\n selectors,\n reactive: categorymanager,\n });\n }\n}\n"],"names":["BaseComponent","create","descriptor","name","element","id","selectors","ADD_BUTTON","stateReady","addEventListener","this","getElement","categorymanager","showEditModal","target","document","querySelector","reactive"],"mappings":"uQA2B6BA,wBAEzBC,OAAOC,iBACEC,KAAOD,WAAWE,QAAQC,QAC1BC,UAAY,CACbC,WAAY,mCAIpBC,kBACSC,iBAAiBC,KAAKC,WAAWD,KAAKJ,UAAUC,YAAa,QAASK,iCAAgBC,2BAUnFC,OAAQR,kBAET,IAAII,KAAK,CACZN,QAFkBW,SAASC,cAAcF,QAGzCR,UAAAA,UACAW,SAAUL"}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
|
||||
define("qbank_managecategories/categorylist",["exports","core/reactive","core/templates","core/str","qbank_managecategories/categorymanager"],(function(_exports,_reactive,_templates,_str,_categorymanager){var obj;Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_templates=(obj=_templates)&&obj.__esModule?obj:{default:obj};class _default extends _reactive.BaseComponent{create(descriptor){this.name=descriptor.element.id,this.selectors={CATEGORY_LIST:".qbank_managecategories-categorylist",CATEGORY_ITEM:".qbank_managecategories-item[data-categoryid]",CATEGORY_CONTENTS:".qbank_managecategories-item > .container",CATEGORY_DETAILS:".qbank_managecategories-details",CATEGORY_NO_DRAGHANDLE:".qbank_managecategories-item[data-categoryid]:not(.draghandle)",CATEGORY_ID:id=>"#category-".concat(id)},this.classes={DROP_TARGET_BEFORE:"qbank_managecategories-droptarget-before",DROP_TARGET:"qbank_managecategories-droptarget",NO_BOTTOM_PADDING:"pb-0"},this.ids={CATEGORY:id=>"category-".concat(id)}}stateReady(){this.dragdrop=new _reactive.DragDrop(this)}destroy(){void 0!==this.dragdrop&&(this.dragdrop.unregister(),this.dragdrop=void 0)}static init(target,selectors){return new this({element:document.querySelector(target),selectors:selectors,reactive:_categorymanager.categorymanager})}validateDropData(){return!0}showDropZone(dropData,event){const dropTarget=this.getElement();return!dropTarget.closest(this.selectors.CATEGORY_ID(dropData.id))&&(this.getInsertBefore(event,dropTarget)?(dropTarget.classList.add(this.classes.DROP_TARGET_BEFORE),dropTarget.classList.remove(this.classes.DROP_TARGET)):(dropTarget.classList.add(this.classes.DROP_TARGET),dropTarget.classList.remove(this.classes.DROP_TARGET_BEFORE)),!0)}hideDropZone(dropData,event){const dropTarget=event.target.closest(this.selectors.CATEGORY_LIST);dropTarget.classList.remove(this.classes.DROP_TARGET_BEFORE),dropTarget.classList.remove(this.classes.DROP_TARGET)}getInsertBefore(event,dropTarget){return event.clientY-dropTarget.getBoundingClientRect().top<dropTarget.clientHeight/2}drop(dropData,event){var _precedingSibling;const dropTarget=event.target.closest(this.selectors.CATEGORY_LIST);if(!dropTarget)return;if(dropTarget.closest(this.selectors.CATEGORY_ID(dropData.id)))return;if(!document.getElementById(this.ids.CATEGORY(dropData.id)))return;const targetParentId=dropTarget.dataset.categoryid;let precedingSibling;precedingSibling=this.getInsertBefore(event,dropTarget)?null:dropTarget.lastElementChild,_categorymanager.categorymanager.moveCategory(dropData.id,targetParentId,null===(_precedingSibling=precedingSibling)||void 0===_precedingSibling?void 0:_precedingSibling.dataset.categoryid)}getWatchers(){return[{watch:"categoryLists[".concat(this.element.dataset.categoryid,"].childCount:updated"),handler:this.checkEmptyList},{watch:"categories:created",handler:this.addCategory}]}async checkEmptyList(_ref){let{element:element}=_ref;if(0===element.childCount){const categoryItem=this.getElement().closest(this.selectors.CATEGORY_ITEM),{html:html,js:js}=await _templates.default.renderForPromise("qbank_managecategories/newchild",{categoryid:this.getElement().dataset.categoryid,tooltip:(0,_str.getString)("newchild","qbank_managecategories",categoryItem.dataset.categoryname)}),activityNameArea=categoryItem.querySelector(this.selectors.CATEGORY_DETAILS);await _templates.default.appendNodeContents(activityNameArea,html,js),this.element.closest(this.selectors.CATEGORY_CONTENTS).classList.remove(this.classes.NO_BOTTOM_PADDING),this.remove()}}async addCategory(_ref2){let{element:element}=_ref2;if(element.parent!==this.getElement().dataset.categoryid)return;const{html:html,js:js}=await _templates.default.renderForPromise("qbank_managecategories/category",element.templatecontext);_templates.default.appendNodeContents(this.getElement(),html,js);const noDragHandle=this.getElement(this.selectors.CATEGORY_NO_DRAGHANDLE);noDragHandle&&this.reactive.dispatch("showDragHandle",noDragHandle.dataset.categoryid)}}return _exports.default=_default,_exports.default}));
|
||||
|
||||
//# sourceMappingURL=categorylist.min.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
|
||||
define("qbank_managecategories/categorymanager",["exports","core/reactive","core/str","qbank_managecategories/mutations","qbank_managecategories/events","core/ajax","core/notification","core_form/modalform"],(function(_exports,_reactive,_str,_mutations,_events,_ajax,_notification,_modalform){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=_exports.categorymanager=void 0,_ajax=_interopRequireDefault(_ajax),_notification=_interopRequireDefault(_notification),_modalform=_interopRequireDefault(_modalform);const SELECTORS_CATEGORY_LIST=".qbank_managecategories-categorylist",SELECTORS_CONTEXT=".qbank_managecategories-categorylist[data-contextid]",SELECTORS_CATEGORY_ITEM=".qbank_managecategories-item[data-categoryid]",SELECTORS_CATEGORY_ROOT="#categoryroot",SELECTORS_SHOWDESCRIPTIONS_TOGGLE="#showdescriptions-toggle",SELECTORS_ADD_EDIT_BUTTON='[data-action="addeditcategory"]',CLASSES_DRAGHANDLE="draghandle",CLASSES_DANGER="alert-danger";class CategoryManager extends _reactive.Reactive{moveCategory(categoryId,targetParentId){let precedingSiblingId=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;const call={methodname:"qbank_managecategories_move_category",args:{pagecontextid:this.state.page.contextid,categoryid:categoryId,targetparentid:targetParentId,precedingsiblingid:precedingSiblingId}};_ajax.default.call([call])[0].then((stateUpdates=>(this.stateManager.processUpdates(stateUpdates),stateUpdates))).catch((error=>{var _document$getElements;_notification.default.addNotification({message:error.message,type:"error"}),null===(_document$getElements=document.getElementsByClassName(CLASSES_DANGER)[0])||void 0===_document$getElements||_document$getElements.scrollIntoView()}))}getTitle(isEdit){return(0,_str.get_string)(isEdit?"editcategory":"addcategory","question")}getSave(isEdit){return isEdit?(0,_str.get_string)("savechanges","core"):(0,_str.get_string)("addcategory","question")}showEditModal(e){const addEditButton=e.target.closest(SELECTORS_ADD_EDIT_BUTTON);if(!addEditButton)return;if(!addEditButton.dataset.actiontype)return;e.preventDefault();const title=categorymanager.getTitle("edit"===addEditButton.dataset.actiontype),save=categorymanager.getSave("edit"===addEditButton.dataset.actiontype),cmid=addEditButton.dataset.cmid,courseid=addEditButton.dataset.courseid,questioncount=addEditButton.dataset.questioncount;let contextid=addEditButton.dataset.contextid,categoryid=null,sortorder=null,parent=null;const categoryItem=e.target.closest(SELECTORS_CATEGORY_ITEM);if(categoryItem){contextid=categoryItem.dataset.contextid,categoryid=categoryItem.dataset.categoryid,sortorder=categoryItem.dataset.sortorder;const parentContext=categoryItem.closest(SELECTORS_CONTEXT);parent=categoryItem.dataset.parent+","+parentContext.dataset.contextid}const modalForm=new _modalform.default({formClass:"qbank_managecategories\\form\\question_category_edit_form",args:{cmid:cmid,courseid:courseid,questioncount:questioncount,contextid:contextid,categoryid:categoryid,sortorder:sortorder,parent:parent},modalConfig:{title:title,large:!0},saveButtonText:save,returnFocus:addEditButton});modalForm.addEventListener(modalForm.events.FORM_SUBMITTED,(response=>{categorymanager.stateManager.processUpdates(response.detail)})),modalForm.show()}}const categorymanager=new CategoryManager({name:"qtype_managecategories_categorymanager",eventName:_events.eventTypes.qbankManagecategoriesStateUpdated,eventDispatch:_events.notifyQbankManagecategoriesStateUpdated,mutations:_mutations.mutations});_exports.categorymanager=categorymanager;_exports.init=()=>{(async reactive=>{const stateData={page:{contextid:document.querySelector(SELECTORS_CATEGORY_ROOT).dataset.contextid,showdescriptions:document.querySelector(SELECTORS_SHOWDESCRIPTIONS_TOGGLE).checked},categories:[],categoryLists:[]};document.querySelectorAll(SELECTORS_CATEGORY_ITEM).forEach((item=>{stateData.categories.push({id:item.dataset.categoryid,name:item.dataset.categoryname,parent:item.dataset.parent,contextid:item.dataset.contextid,sortorder:item.dataset.sortorder,draghandle:item.classList.contains(CLASSES_DRAGHANDLE)})})),document.querySelectorAll(SELECTORS_CATEGORY_LIST).forEach((categoryList=>{stateData.categoryLists.push({id:categoryList.dataset.categoryid,childCount:categoryList.querySelectorAll(SELECTORS_CATEGORY_ITEM).length})})),reactive.setInitialState(stateData)})(categorymanager)}}));
|
||||
|
||||
//# sourceMappingURL=categorymanager.min.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
|
||||
define("qbank_managecategories/categoryroot",["exports","core/reactive","qbank_managecategories/categorymanager"],(function(_exports,_reactive,_categorymanager){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;class _default extends _reactive.BaseComponent{create(descriptor){this.name=descriptor.element.id,this.classes={SHOWDESCRIPTIONS:"showdescriptions"}}static init(target,selectors){return new this({element:document.querySelector(target),selectors:selectors,reactive:_categorymanager.categorymanager})}getWatchers(){return[{watch:"page.showdescriptions:updated",handler:this.toggleDescriptions}]}toggleDescriptions(_ref){let{element:element}=_ref;element.showdescriptions?this.getElement().classList.add(this.classes.SHOWDESCRIPTIONS):this.getElement().classList.remove(this.classes.SHOWDESCRIPTIONS)}}return _exports.default=_default,_exports.default}));
|
||||
|
||||
//# sourceMappingURL=categoryroot.min.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"categoryroot.min.js","sources":["../src/categoryroot.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 <http://www.gnu.org/licenses/>.\n\n/**\n * The category root component.\n *\n * @module qbank_managecategories/categoryroot\n * @class qbank_managecategories/categoryroot\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {categorymanager} from 'qbank_managecategories/categorymanager';\n\nexport default class extends BaseComponent {\n\n create(descriptor) {\n this.name = descriptor.element.id;\n this.classes = {\n SHOWDESCRIPTIONS: 'showdescriptions',\n };\n }\n\n /**\n * Static method to create a component instance.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.querySelector(target),\n selectors,\n reactive: categorymanager,\n });\n }\n\n /**\n * Watch for changes to the page state.\n *\n * @return {Array} A list of watchers.\n */\n getWatchers() {\n return [\n // Watch for descriptions being toggled.\n {watch: `page.showdescriptions:updated`, handler: this.toggleDescriptions}\n ];\n }\n\n /**\n * Show or hide descriptions when the flag in the state is changed.\n *\n * @param {Object} args\n * @param {Object} args.element The updated page state.\n */\n toggleDescriptions({element}) {\n if (element.showdescriptions) {\n this.getElement().classList.add(this.classes.SHOWDESCRIPTIONS);\n } else {\n this.getElement().classList.remove(this.classes.SHOWDESCRIPTIONS);\n }\n }\n}\n"],"names":["BaseComponent","create","descriptor","name","element","id","classes","SHOWDESCRIPTIONS","target","selectors","this","document","querySelector","reactive","categorymanager","getWatchers","watch","handler","toggleDescriptions","showdescriptions","getElement","classList","add","remove"],"mappings":"wQAyB6BA,wBAEzBC,OAAOC,iBACEC,KAAOD,WAAWE,QAAQC,QAC1BC,QAAU,CACXC,iBAAkB,gCAWdC,OAAQC,kBACT,IAAIC,KAAK,CACZN,QAASO,SAASC,cAAcJ,QAChCC,UAAAA,UACAI,SAAUC,mCASlBC,oBACW,CAEH,CAACC,sCAAwCC,QAASP,KAAKQ,qBAU/DA,6BAAmBd,QAACA,cACZA,QAAQe,sBACHC,aAAaC,UAAUC,IAAIZ,KAAKJ,QAAQC,uBAExCa,aAAaC,UAAUE,OAAOb,KAAKJ,QAAQC"}
|
||||
@@ -0,0 +1,3 @@
|
||||
define("qbank_managecategories/events",["exports","core/event_dispatcher"],(function(_exports,_event_dispatcher){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.notifyQbankManagecategoriesStateUpdated=_exports.eventTypes=void 0;const eventTypes={qbankManagecategoriesStateUpdated:"qbank_managecategories/stateUpdated"};_exports.eventTypes=eventTypes;_exports.notifyQbankManagecategoriesStateUpdated=(detail,container)=>(0,_event_dispatcher.dispatchEvent)(eventTypes.qbankManagecategoriesStateUpdated,detail,container)}));
|
||||
|
||||
//# sourceMappingURL=events.min.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"events.min.js","sources":["../src/events.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 <http://www.gnu.org/licenses/>.\n\nimport {dispatchEvent} from 'core/event_dispatcher';\n\n/**\n * Reactive events for qbank_managecategories\n *\n * @module qbank_managecategories/events\n *\n */\nexport const eventTypes = {\n /**\n * Event triggered when the activity reactive state is updated.\n *\n * @event qbankManagecategoriesStateUpdated\n * @type {CustomEvent}\n * @property {Array} nodes The list of parent nodes which were updated\n */\n qbankManagecategoriesStateUpdated: 'qbank_managecategories/stateUpdated',\n};\n\n/**\n * Trigger an event to indicate that the activity state is updated.\n *\n * @method notifyQbankManagecategoriesStateUpdated\n * @param {Object} detail the full state\n * @param {HTMLElement} container the custom event target (document if none provided)\n * @returns {CustomEvent}\n * @fires qbankManagecategoriesStateUpdated\n */\nexport const notifyQbankManagecategoriesStateUpdated = (detail, container) => {\n return dispatchEvent(eventTypes.qbankManagecategoriesStateUpdated, detail, container);\n};\n"],"names":["eventTypes","qbankManagecategoriesStateUpdated","detail","container"],"mappings":"2PAuBaA,WAAa,CAQtBC,kCAAmC,uHAYgB,CAACC,OAAQC,aACrD,mCAAcH,WAAWC,kCAAmCC,OAAQC"}
|
||||
@@ -0,0 +1,3 @@
|
||||
define("qbank_managecategories/mutations",["exports","core_user/repository"],(function(_exports,_repository){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.mutations=void 0;const mutations=new class{async toggleDescriptions(stateManager,showDescriptions){stateManager.setReadOnly(!1),await(0,_repository.setUserPreference)("qbank_managecategories_showdescriptions",showDescriptions),stateManager.state.page.showdescriptions=showDescriptions,stateManager.setReadOnly(!0)}async showDragHandle(stateManager,categoryId){stateManager.setReadOnly(!1),stateManager.state.categories.get(categoryId).draghandle=!0,stateManager.setReadOnly(!0)}};_exports.mutations=mutations}));
|
||||
|
||||
//# sourceMappingURL=mutations.min.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"mutations.min.js","sources":["../src/mutations.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 <http://www.gnu.org/licenses/>.\n\nimport {setUserPreference} from 'core_user/repository';\n\n/**\n * Default mutation manager\n *\n * @module qbank_managecategories/mutations\n */\nclass Mutations {\n /**\n * Set the showdescriptions property to true or false in the page's state.\n *\n * @param {stateManager} stateManager\n * @param {boolean} showDescriptions\n * @return {Promise<void>}\n */\n async toggleDescriptions(stateManager, showDescriptions) {\n stateManager.setReadOnly(false);\n await setUserPreference('qbank_managecategories_showdescriptions', showDescriptions);\n stateManager.state.page.showdescriptions = showDescriptions;\n stateManager.setReadOnly(true);\n }\n\n /**\n * Set the draghandle property to true in a given category's state.\n *\n * @param {stateManager} stateManager\n * @param {Number} categoryId\n * @return {Promise<void>}\n */\n async showDragHandle(stateManager, categoryId) {\n stateManager.setReadOnly(false);\n stateManager.state.categories.get(categoryId).draghandle = true;\n stateManager.setReadOnly(true);\n }\n}\n\nexport const mutations = new Mutations();\n"],"names":["mutations","stateManager","showDescriptions","setReadOnly","state","page","showdescriptions","categoryId","categories","get","draghandle"],"mappings":"qMAmDaA,UAAY,mCArBIC,aAAcC,kBACnCD,aAAaE,aAAY,SACnB,iCAAkB,0CAA2CD,kBACnED,aAAaG,MAAMC,KAAKC,iBAAmBJ,iBAC3CD,aAAaE,aAAY,wBAURF,aAAcM,YAC/BN,aAAaE,aAAY,GACzBF,aAAaG,MAAMI,WAAWC,IAAIF,YAAYG,YAAa,EAC3DT,aAAaE,aAAY"}
|
||||
@@ -0,0 +1,3 @@
|
||||
define("qbank_managecategories/newchild",["exports","core/reactive","jquery","qbank_managecategories/categorymanager"],(function(_exports,_reactive,_jquery,_categorymanager){var obj;Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_jquery=(obj=_jquery)&&obj.__esModule?obj:{default:obj};class _default extends _reactive.BaseComponent{create(descriptor){this.name=descriptor.element.id,this.selectors={NEW_CHILD:".qbank_managecategories-newchild",CATEGORY_ID:id=>"#category-".concat(id)},this.classes={DROP_TARGET:"qbank_managecategories-droptarget"},this.ids={CATEGORY:id=>"category-".concat(id)}}stateReady(){this.dragdrop=new _reactive.DragDrop(this)}destroy(){void 0!==this.dragdrop&&(this.dragdrop.unregister(),this.dragdrop=void 0)}static init(target,selectors){return new this({element:document.querySelector(target),selectors:selectors,reactive:_categorymanager.categorymanager})}validateDropData(dropData){return!this.getElement().closest(this.selectors.CATEGORY_ID(dropData.id))}showDropZone(dropData,event){const dropTarget=event.target.closest(this.selectors.NEW_CHILD);dropTarget.classList.add(this.classes.DROP_TARGET),(0,_jquery.default)(dropTarget).tooltip("show")}hideDropZone(dropData,event){const dropTarget=event.target.closest(this.selectors.NEW_CHILD);dropTarget.classList.remove(this.classes.DROP_TARGET),(0,_jquery.default)(dropTarget).tooltip("hide")}drop(dropData,event){const dropTarget=event.target.closest(this.selectors.NEW_CHILD);if(!dropTarget)return;if(!document.getElementById(this.ids.CATEGORY(dropData.id)))return;const targetParentId=dropTarget.dataset.parent;_categorymanager.categorymanager.moveCategory(dropData.id,targetParentId)}getWatchers(){return[{watch:"categories.parent:updated",handler:this.checkNewChild}]}checkNewChild(_ref){let{element:element}=_ref;element.parent===parseInt(this.element.dataset.parent)&&this.remove()}}return _exports.default=_default,_exports.default}));
|
||||
|
||||
//# sourceMappingURL=newchild.min.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
|
||||
define("qbank_managecategories/showdescriptions",["exports","core/reactive","qbank_managecategories/categorymanager"],(function(_exports,_reactive,_categorymanager){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;class _default extends _reactive.BaseComponent{create(descriptor){this.name=descriptor.element.id,this.selectors={TOGGLE:"#showdescriptions-toggle"}}stateReady(){this.addEventListener(this.getElement(this.selectors.TOGGLE),"change",this.updateShowDescriptions)}static init(target,selectors){return new this({element:document.querySelector(target),selectors:selectors,reactive:_categorymanager.categorymanager})}async updateShowDescriptions(event){const checked=event.target.checked;this.reactive.dispatch("toggleDescriptions",checked)}}return _exports.default=_default,_exports.default}));
|
||||
|
||||
//# sourceMappingURL=showdescriptions.min.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"showdescriptions.min.js","sources":["../src/showdescriptions.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 <http://www.gnu.org/licenses/>.\n\n/**\n * The show descriptions toggle component.\n *\n * @module qbank_managecategories/showdescriptions\n * @class qbank_managecategories/showdescriptions\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {categorymanager} from 'qbank_managecategories/categorymanager';\n\nexport default class extends BaseComponent {\n\n create(descriptor) {\n this.name = descriptor.element.id;\n this.selectors = {\n TOGGLE: '#showdescriptions-toggle',\n };\n }\n\n stateReady() {\n this.addEventListener(this.getElement(this.selectors.TOGGLE), 'change', this.updateShowDescriptions);\n }\n\n /**\n * Static method to create a component instance.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n const targetElement = document.querySelector(target);\n return new this({\n element: targetElement,\n selectors,\n reactive: categorymanager,\n });\n }\n\n /**\n * Dispatch a mutation to toggle the showDescriptions setting.\n *\n * @param {Event} event The toggle change event.\n * @return {Promise<void>}\n */\n async updateShowDescriptions(event) {\n const checked = event.target.checked;\n this.reactive.dispatch('toggleDescriptions', checked);\n }\n}\n"],"names":["BaseComponent","create","descriptor","name","element","id","selectors","TOGGLE","stateReady","addEventListener","this","getElement","updateShowDescriptions","target","document","querySelector","reactive","categorymanager","event","checked","dispatch"],"mappings":"4QAyB6BA,wBAEzBC,OAAOC,iBACEC,KAAOD,WAAWE,QAAQC,QAC1BC,UAAY,CACbC,OAAQ,4BAIhBC,kBACSC,iBAAiBC,KAAKC,WAAWD,KAAKJ,UAAUC,QAAS,SAAUG,KAAKE,oCAUrEC,OAAQP,kBAET,IAAII,KAAK,CACZN,QAFkBU,SAASC,cAAcF,QAGzCP,UAAAA,UACAU,SAAUC,gEAUWC,aACnBC,QAAUD,MAAML,OAAOM,aACxBH,SAASI,SAAS,qBAAsBD"}
|
||||
@@ -0,0 +1,56 @@
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Add category button for displaying the modal form.
|
||||
*
|
||||
* This just connects up the button to the showEditModal listener.
|
||||
*
|
||||
* @module qbank_managecategories/addcategory
|
||||
* @class qbank_managecategories/addcategory
|
||||
*/
|
||||
|
||||
import {BaseComponent} from 'core/reactive';
|
||||
import {categorymanager} from 'qbank_managecategories/categorymanager';
|
||||
|
||||
export default class extends BaseComponent {
|
||||
|
||||
create(descriptor) {
|
||||
this.name = descriptor.element.id;
|
||||
this.selectors = {
|
||||
ADD_BUTTON: '[data-action="addeditcategory"]',
|
||||
};
|
||||
}
|
||||
|
||||
stateReady() {
|
||||
this.addEventListener(this.getElement(this.selectors.ADD_BUTTON), 'click', categorymanager.showEditModal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Static method to create a component instance form the mustache template.
|
||||
*
|
||||
* @param {string} target the DOM main element or its ID
|
||||
* @param {object} selectors optional css selector overrides
|
||||
* @return {Component}
|
||||
*/
|
||||
static init(target, selectors) {
|
||||
const targetElement = document.querySelector(target);
|
||||
return new this({
|
||||
element: targetElement,
|
||||
selectors,
|
||||
reactive: categorymanager,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,469 @@
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* The category component.
|
||||
*
|
||||
* @module qbank_managecategories/category
|
||||
* @class qbank_managecategories/category
|
||||
*/
|
||||
|
||||
import {BaseComponent, DragDrop} from 'core/reactive';
|
||||
import {categorymanager} from 'qbank_managecategories/categorymanager';
|
||||
import Templates from 'core/templates';
|
||||
import Modal from "core/modal";
|
||||
import {get_string as getString} from "core/str";
|
||||
|
||||
export default class extends BaseComponent {
|
||||
|
||||
create(descriptor) {
|
||||
this.name = descriptor.element.id;
|
||||
this.selectors = {
|
||||
CATEGORY_LIST: '.qbank_managecategories-categorylist',
|
||||
CATEGORY_ITEM: '.qbank_managecategories-item[data-categoryid]',
|
||||
CATEGORY_CONTENTS: '.qbank_managecategories-item > .container',
|
||||
EDIT_BUTTON: '[data-action="addeditcategory"]',
|
||||
MOVE_BUTTON: '[role="menuitem"][data-actiontype="move"]',
|
||||
CONTEXT: '.qbank_managecategories-categorylist[data-contextid]',
|
||||
MODAL_CATEGORY_ITEM: '.modal_category_item[data-movingcategoryid]',
|
||||
CONTENT_AREA: '.qbank_managecategories-details',
|
||||
CATEGORY_ID: id => `#category-${id}`,
|
||||
CONTENT_CONTAINER: id => `#category-${id} .qbank_managecategories-childlistcontainer`,
|
||||
CHILD_LIST: id => `ul[data-categoryid="${id}"]`,
|
||||
PREVIOUS_SIBLING: sortorder => `:scope > [data-sortorder="${sortorder}"]`,
|
||||
};
|
||||
this.classes = {
|
||||
NO_BOTTOM_PADDING: 'pb-0',
|
||||
DRAGHANDLE: 'draghandle',
|
||||
DROPTARGET: 'qbank_managecategories-droptarget-before',
|
||||
};
|
||||
this.ids = {
|
||||
CATEGORY: id => `category-${id}`,
|
||||
};
|
||||
}
|
||||
|
||||
stateReady() {
|
||||
this.initDragDrop();
|
||||
this.addEventListener(this.getElement(this.selectors.EDIT_BUTTON), 'click', categorymanager.showEditModal);
|
||||
const moveButton = this.getElement(this.selectors.MOVE_BUTTON);
|
||||
this.addEventListener(moveButton, 'click', this.showMoveModal);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
// The draggable element must be unregistered.
|
||||
this.deInitDragDrop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove any existing DragDrop component, and create a new one.
|
||||
*/
|
||||
initDragDrop() {
|
||||
this.deInitDragDrop();
|
||||
// If the element is currently draggable, register the getDraggableData method.
|
||||
if (this.element.classList.contains(this.classes.DRAGHANDLE)) {
|
||||
this.getDraggableData = this._getDraggableData;
|
||||
}
|
||||
this.dragdrop = new DragDrop(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the DragDrop component is currently registered, unregister it.
|
||||
*/
|
||||
deInitDragDrop() {
|
||||
if (this.dragdrop !== undefined) {
|
||||
if (this.getDraggableData !== undefined) {
|
||||
this.dragdrop.setDraggable(false);
|
||||
this.getDraggableData = undefined;
|
||||
}
|
||||
this.dragdrop.unregister();
|
||||
this.dragdrop = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Static method to create a component instance.
|
||||
*
|
||||
* @param {string} target the DOM main element or its ID
|
||||
* @param {object} selectors optional css selector overrides
|
||||
* @return {Component}
|
||||
*/
|
||||
static init(target, selectors) {
|
||||
return new this({
|
||||
element: document.querySelector(target),
|
||||
selectors,
|
||||
reactive: categorymanager,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the category ID from the component's element.
|
||||
*
|
||||
* This method is referenced as getDraggableData when the component can be dragged.
|
||||
*
|
||||
* @return {{id: string}}
|
||||
* @private
|
||||
*/
|
||||
_getDraggableData() {
|
||||
return {
|
||||
id: this.getElement().dataset.categoryid
|
||||
};
|
||||
}
|
||||
|
||||
validateDropData() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlight the top border of the category item.
|
||||
*
|
||||
* @param {Object} dropData
|
||||
*/
|
||||
showDropZone(dropData) {
|
||||
if (this.getElement().closest(this.selectors.CATEGORY_ID(dropData.id))) {
|
||||
// Can't drop onto itself or its own child.
|
||||
return false;
|
||||
}
|
||||
this.getElement().classList.add(this.classes.DROPTARGET);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove highlighting.
|
||||
*/
|
||||
hideDropZone() {
|
||||
this.getElement().classList.remove(this.classes.DROPTARGET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the new position of the dropped category, and trigger the move.
|
||||
*
|
||||
* @param {Object} dropData The category being moved.
|
||||
* @param {Event} event The drop event.
|
||||
*/
|
||||
drop(dropData, event) {
|
||||
const dropTarget = event.target.closest(this.selectors.CATEGORY_ITEM);
|
||||
|
||||
if (!dropTarget) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (dropTarget.closest(this.selectors.CATEGORY_ID(dropData.id))) {
|
||||
// Can't drop onto your own child.
|
||||
return;
|
||||
}
|
||||
|
||||
const source = document.getElementById(this.ids.CATEGORY(dropData.id));
|
||||
|
||||
if (!source) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetParentId = dropTarget.dataset.parent;
|
||||
const parentList = dropTarget.closest(this.selectors.CATEGORY_LIST);
|
||||
let precedingSibling;
|
||||
|
||||
if (dropTarget === parentList.firstElementChild) {
|
||||
// Dropped at the top of the list.
|
||||
precedingSibling = null;
|
||||
} else {
|
||||
precedingSibling = dropTarget.previousElementSibling;
|
||||
}
|
||||
|
||||
// Insert the category after the target category
|
||||
categorymanager.moveCategory(dropData.id, targetParentId, precedingSibling?.dataset.categoryid);
|
||||
}
|
||||
|
||||
getWatchers() {
|
||||
return [
|
||||
// After any update to this category, move it to the new position.
|
||||
{watch: `categories[${this.element.dataset.categoryid}]:updated`, handler: this.updatePosition},
|
||||
// When the template context is added or updated, re-render the content.
|
||||
{watch: `categories[${this.element.dataset.categoryid}].templatecontext:created`, handler: this.rerender},
|
||||
{watch: `categories[${this.element.dataset.categoryid}].templatecontext:updated`, handler: this.rerender},
|
||||
// When a new category is created, check whether we need to add a child list to this category.
|
||||
{watch: `categories:created`, handler: this.checkChildList},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-render the category content.
|
||||
*
|
||||
* @param {Object} args
|
||||
* @param {Element} args.element
|
||||
* @return {Promise<Array>}
|
||||
*/
|
||||
async rerender({element}) {
|
||||
const {html, js} = await Templates.renderForPromise(
|
||||
'qbank_managecategories/category_details',
|
||||
element.templatecontext
|
||||
);
|
||||
return Templates.replaceNodeContents(this.getElement(this.selectors.CONTENT_AREA), html, js);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render and append a new child list.
|
||||
*
|
||||
* @param {Object} context Template context, must include at least categoryid.
|
||||
* @return {Promise<Element>}
|
||||
*/
|
||||
async createChildList(context) {
|
||||
const {html, js} = await Templates.renderForPromise(
|
||||
'qbank_managecategories/childlist',
|
||||
context,
|
||||
);
|
||||
const parentContainer = document.querySelector(this.selectors.CONTENT_CONTAINER(context.categoryid));
|
||||
await Templates.appendNodeContents(parentContainer, html, js);
|
||||
const childList = document.querySelector(this.selectors.CHILD_LIST(context.categoryid));
|
||||
childList.closest(this.selectors.CATEGORY_CONTENTS).classList.add(this.classes.NO_BOTTOM_PADDING);
|
||||
return childList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a category to its new position.
|
||||
*
|
||||
* A category may change its parent, sortorder and draghandle independently or at the same time. This method will resolve those
|
||||
* changes and move the element to the new position. If the parent doesn't already have a child list, one will be created.
|
||||
*
|
||||
* If the parent has changed, this will also update the state with the new child count of the old and new parents.
|
||||
*
|
||||
* @param {Object} args
|
||||
* @param {Object} args.element
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async updatePosition({element}) {
|
||||
window.console.log('updating', this.getElement());
|
||||
window.console.log('new state', element);
|
||||
// Move to a new parent category.
|
||||
let newParent;
|
||||
const originParent = document.querySelector(this.selectors.CHILD_LIST(this.getElement().dataset.parent));
|
||||
if (parseInt(this.getElement().dataset.parent) !== element.parent) {
|
||||
newParent = document.querySelector(this.selectors.CHILD_LIST(element.parent));
|
||||
if (!newParent) {
|
||||
// The target category doesn't have a child list yet. We'd better create one.
|
||||
newParent = await this.createChildList({categoryid: element.parent});
|
||||
}
|
||||
this.getElement().dataset.parent = element.parent;
|
||||
} else {
|
||||
newParent = this.getElement().parentElement;
|
||||
}
|
||||
window.console.log('newParent', newParent);
|
||||
|
||||
// Move to a new position within the parent.
|
||||
let previousSibling;
|
||||
let nextSibling;
|
||||
if (newParent.firstElementChild && parseInt(element.sortorder) <= parseInt(newParent.firstElementChild.dataset.sortorder)) {
|
||||
// Move to the top of the list.
|
||||
nextSibling = newParent.firstElementChild;
|
||||
} else {
|
||||
// Move later in the list.
|
||||
previousSibling = newParent.querySelector(this.selectors.PREVIOUS_SIBLING(element.sortorder - 1));
|
||||
nextSibling = previousSibling?.nextElementSibling;
|
||||
}
|
||||
window.console.log('previousSibling', previousSibling);
|
||||
window.console.log('nextSibling', nextSibling);
|
||||
|
||||
// Check if this has actually moved, or if it's just having its sortorder updated due to another element moving.
|
||||
const moved = (newParent !== this.getElement().parentElement || nextSibling !== this.getElement());
|
||||
|
||||
window.console.log('moved', moved);
|
||||
|
||||
if (moved) {
|
||||
if (nextSibling) {
|
||||
window.console.log('insertBefore');
|
||||
// Move to the specified position in the list.
|
||||
newParent.insertBefore(this.getElement(), nextSibling);
|
||||
} else {
|
||||
window.console.log('appendChild');
|
||||
// Move to the end of the list (may also be the top of the list is empty).
|
||||
newParent.appendChild(this.getElement());
|
||||
}
|
||||
}
|
||||
if (originParent !== newParent) {
|
||||
// Update child count of old and new parent.
|
||||
this.reactive.stateManager.processUpdates([
|
||||
{
|
||||
name: 'categoryLists',
|
||||
action: 'put',
|
||||
fields: {
|
||||
id: originParent.dataset.categoryid,
|
||||
childCount: originParent.querySelectorAll(this.selectors.CATEGORY_ITEM).length
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'categoryLists',
|
||||
action: 'put',
|
||||
fields: {
|
||||
id: newParent.dataset.categoryid,
|
||||
childCount: newParent.querySelectorAll(this.selectors.CATEGORY_ITEM).length
|
||||
}
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
this.element.dataset.sortorder = element.sortorder;
|
||||
|
||||
// Enable/disable dragging.
|
||||
const isDraggable = this.element.classList.contains(this.classes.DRAGHANDLE);
|
||||
if (isDraggable && !element.draghandle) {
|
||||
this.element.classList.remove(this.classes.DRAGHANDLE);
|
||||
this.initDragDrop();
|
||||
} else if (!isDraggable && element.draghandle) {
|
||||
this.element.classList.add(this.classes.DRAGHANDLE);
|
||||
this.initDragDrop();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively create a list of all valid destinations for a current category within a parent category.
|
||||
*
|
||||
* @param {Element} item
|
||||
* @param {Number} movingCategoryId
|
||||
* @return {Array<Object>}
|
||||
*/
|
||||
createMoveCategoryList(item, movingCategoryId) {
|
||||
const categories = [];
|
||||
if (item.children) {
|
||||
let precedingSibling = null;
|
||||
item.children.forEach(category => {
|
||||
const categoryId = parseInt(category.dataset.categoryid);
|
||||
// Don't create a target for the category that's moving.
|
||||
if (categoryId === movingCategoryId) {
|
||||
return;
|
||||
}
|
||||
// Create a target to move before this child.
|
||||
let child = {
|
||||
categoryid: categoryId,
|
||||
movingcategoryid: movingCategoryId,
|
||||
precedingsiblingid: precedingSibling?.dataset.categoryid ?? 0,
|
||||
parent: category.dataset.parent,
|
||||
categoryname: category.dataset.categoryname,
|
||||
categories: null,
|
||||
current: categoryId === movingCategoryId,
|
||||
};
|
||||
const childList = category.querySelector(this.selectors.CATEGORY_LIST);
|
||||
if (childList) {
|
||||
// If the child has its own children, recursively make a list of those.
|
||||
child.categories = this.createMoveCategoryList(childList, movingCategoryId);
|
||||
} else {
|
||||
// Otherwise, create a target to move as a new child of this one.
|
||||
child.categories = [
|
||||
{
|
||||
movingcategoryid: movingCategoryId,
|
||||
precedingsiblingid: 0,
|
||||
parent: categoryId,
|
||||
categoryname: category.dataset.categoryname,
|
||||
categories: null,
|
||||
newchild: true,
|
||||
}
|
||||
];
|
||||
}
|
||||
categories.push(child);
|
||||
precedingSibling = category;
|
||||
});
|
||||
const precedingId = parseInt(precedingSibling.dataset.categoryid);
|
||||
if (precedingId !== movingCategoryId) {
|
||||
// If this is the last child of its parent, also create a target to move the category after this one.
|
||||
categories.push({
|
||||
movingcategoryid: movingCategoryId,
|
||||
precedingsiblingid: precedingId,
|
||||
parent: precedingSibling.dataset.parent,
|
||||
categoryname: precedingSibling.dataset.categoryname,
|
||||
categories: null,
|
||||
lastchild: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
return categories;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a modal containing links to move the category to a new location.
|
||||
*
|
||||
* @param {Event} e Button click event.
|
||||
*/
|
||||
async showMoveModal(e) {
|
||||
// Return if it is not menu item.
|
||||
const item = e.target;
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
// Return if it is disabled.
|
||||
if (item.getAttribute('aria-disabled') === 'true') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent addition click on the item.
|
||||
item.setAttribute('aria-disabled', true);
|
||||
|
||||
// Build the list of move links.
|
||||
let moveList = {contexts: []};
|
||||
const contexts = document.querySelectorAll(this.selectors.CONTEXT);
|
||||
contexts.forEach(context => {
|
||||
const moveContext = {
|
||||
contextname: context.dataset.contextname,
|
||||
categories: [],
|
||||
hascategories: false,
|
||||
};
|
||||
moveContext.categories = this.createMoveCategoryList(context, parseInt(item.dataset.categoryid));
|
||||
moveContext.hascategories = moveContext.categories.length > 0;
|
||||
moveList.contexts.push(moveContext);
|
||||
});
|
||||
|
||||
const modal = await Modal.create({
|
||||
title: getString('movecategory', 'qbank_managecategories', item.dataset.categoryname),
|
||||
body: Templates.render('qbank_managecategories/move_context_list', moveList),
|
||||
footer: '',
|
||||
show: true,
|
||||
large: true,
|
||||
});
|
||||
// Show modal and add click event for list items.
|
||||
modal.getBody()[0].addEventListener('click', e => {
|
||||
const target = e.target.closest(this.selectors.MODAL_CATEGORY_ITEM);
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
categorymanager.moveCategory(target.dataset.movingcategoryid, target.dataset.parent, target.dataset.precedingsiblingid);
|
||||
modal.destroy();
|
||||
});
|
||||
item.setAttribute('aria-disabled', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check and add a child list if needed.
|
||||
*
|
||||
* Check whether the category that has just been added has this category as its parent. If it does,
|
||||
* check that this category has a child list, and if not, add one.
|
||||
*
|
||||
* @param {Object} args
|
||||
* @param {Element} args.element The new category.
|
||||
* @return {Promise<Element>}
|
||||
*/
|
||||
async checkChildList({element}) {
|
||||
if (element.parent !== this.getElement().dataset.categoryid) {
|
||||
return null; // Not for me.
|
||||
}
|
||||
let childList = this.getElement(this.selectors.CATEGORY_LIST);
|
||||
if (childList) {
|
||||
return null; // List already exists, it will handle adding the new category.
|
||||
}
|
||||
// Render and add a new child list containing the new category.
|
||||
return this.createChildList({
|
||||
categoryid: element.parent,
|
||||
children: [
|
||||
element.templatecontext,
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* The category list component.
|
||||
*
|
||||
* The category list is a drop target, so that a category may be dropped at the top or bottom of the list.
|
||||
*
|
||||
* @module qbank_managecategories/categorylist
|
||||
* @class qbank_managecategories/categorylist
|
||||
*/
|
||||
|
||||
import {BaseComponent, DragDrop} from 'core/reactive';
|
||||
import Templates from 'core/templates';
|
||||
import {getString} from 'core/str';
|
||||
import {categorymanager} from 'qbank_managecategories/categorymanager';
|
||||
|
||||
export default class extends BaseComponent {
|
||||
|
||||
create(descriptor) {
|
||||
this.name = descriptor.element.id;
|
||||
this.selectors = {
|
||||
CATEGORY_LIST: '.qbank_managecategories-categorylist',
|
||||
CATEGORY_ITEM: '.qbank_managecategories-item[data-categoryid]',
|
||||
CATEGORY_CONTENTS: '.qbank_managecategories-item > .container',
|
||||
CATEGORY_DETAILS: '.qbank_managecategories-details',
|
||||
CATEGORY_NO_DRAGHANDLE: '.qbank_managecategories-item[data-categoryid]:not(.draghandle)',
|
||||
CATEGORY_ID: id => `#category-${id}`,
|
||||
};
|
||||
this.classes = {
|
||||
DROP_TARGET_BEFORE: 'qbank_managecategories-droptarget-before',
|
||||
DROP_TARGET: 'qbank_managecategories-droptarget',
|
||||
NO_BOTTOM_PADDING: 'pb-0',
|
||||
};
|
||||
this.ids = {
|
||||
CATEGORY: id => `category-${id}`,
|
||||
};
|
||||
}
|
||||
|
||||
stateReady() {
|
||||
this.dragdrop = new DragDrop(this);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
// The draggable element must be unregistered.
|
||||
if (this.dragdrop !== undefined) {
|
||||
this.dragdrop.unregister();
|
||||
this.dragdrop = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Static method to create a component instance.
|
||||
*
|
||||
* @param {string} target the DOM main element or its ID
|
||||
* @param {object} selectors optional css selector overrides
|
||||
* @return {Component}
|
||||
*/
|
||||
static init(target, selectors) {
|
||||
return new this({
|
||||
element: document.querySelector(target),
|
||||
selectors,
|
||||
reactive: categorymanager,
|
||||
});
|
||||
}
|
||||
|
||||
validateDropData() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlight the border of the list where the category will be moved.
|
||||
*
|
||||
* If dropping at the top of the list, highlight the top border.
|
||||
* If dropping at the bottom, highlight the bottom border.
|
||||
*
|
||||
* @param {Object} dropData
|
||||
* @param {Event} event
|
||||
*/
|
||||
showDropZone(dropData, event) {
|
||||
const dropTarget = this.getElement();
|
||||
if (dropTarget.closest(this.selectors.CATEGORY_ID(dropData.id))) {
|
||||
// Can't drop onto its own child.
|
||||
return false;
|
||||
}
|
||||
if (this.getInsertBefore(event, dropTarget)) {
|
||||
dropTarget.classList.add(this.classes.DROP_TARGET_BEFORE);
|
||||
dropTarget.classList.remove(this.classes.DROP_TARGET);
|
||||
} else {
|
||||
dropTarget.classList.add(this.classes.DROP_TARGET);
|
||||
dropTarget.classList.remove(this.classes.DROP_TARGET_BEFORE);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove highlighting.
|
||||
*
|
||||
* @param {Object} dropData
|
||||
* @param {Event} event
|
||||
*/
|
||||
hideDropZone(dropData, event) {
|
||||
const dropTarget = event.target.closest(this.selectors.CATEGORY_LIST);
|
||||
dropTarget.classList.remove(this.classes.DROP_TARGET_BEFORE);
|
||||
dropTarget.classList.remove(this.classes.DROP_TARGET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether we're dragging over the top or bottom half of the list.
|
||||
*
|
||||
* @param {Event} event
|
||||
* @param {Element} dropTarget
|
||||
* @return {boolean}
|
||||
*/
|
||||
getInsertBefore(event, dropTarget) {
|
||||
// Get the current mouse position within the drop target
|
||||
const mouseY = event.clientY - dropTarget.getBoundingClientRect().top;
|
||||
|
||||
// Get the height of the drop target
|
||||
const targetHeight = dropTarget.clientHeight;
|
||||
|
||||
// Check if the mouse is over the top half of the drop target
|
||||
return mouseY < targetHeight / 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the new position of the dropped category, and trigger the move.
|
||||
*
|
||||
* @param {Object} dropData
|
||||
* @param {Event} event
|
||||
*/
|
||||
drop(dropData, event) {
|
||||
const dropTarget = event.target.closest(this.selectors.CATEGORY_LIST);
|
||||
|
||||
if (!dropTarget) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (dropTarget.closest(this.selectors.CATEGORY_ID(dropData.id))) {
|
||||
// Can't drop onto your own child.
|
||||
return;
|
||||
}
|
||||
|
||||
const source = document.getElementById(this.ids.CATEGORY(dropData.id));
|
||||
|
||||
if (!source) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetParentId = dropTarget.dataset.categoryid;
|
||||
let precedingSibling;
|
||||
|
||||
if (this.getInsertBefore(event, dropTarget)) {
|
||||
// Dropped at the top of the list.
|
||||
precedingSibling = null;
|
||||
} else {
|
||||
// Dropped at the bottom of the list.
|
||||
precedingSibling = dropTarget.lastElementChild;
|
||||
}
|
||||
|
||||
// Insert the category after the target category
|
||||
categorymanager.moveCategory(dropData.id, targetParentId, precedingSibling?.dataset.categoryid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch for categories moving to a new parent.
|
||||
*
|
||||
* @return {Array} A list of watchers.
|
||||
*/
|
||||
getWatchers() {
|
||||
return [
|
||||
// Watch for this category having its child count updated.
|
||||
{watch: `categoryLists[${this.element.dataset.categoryid}].childCount:updated`, handler: this.checkEmptyList},
|
||||
// Watch for any new category being created.
|
||||
{watch: `categories:created`, handler: this.addCategory},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* If this list is now empty, remove it.
|
||||
*
|
||||
* @param {Object} args
|
||||
* @param {Object} args.element The categoryList state element.
|
||||
*/
|
||||
async checkEmptyList({element}) {
|
||||
if (element.childCount === 0) {
|
||||
// Display a new child drop zone.
|
||||
const categoryItem = this.getElement().closest(this.selectors.CATEGORY_ITEM);
|
||||
const {html, js} = await Templates.renderForPromise(
|
||||
'qbank_managecategories/newchild',
|
||||
{
|
||||
categoryid: this.getElement().dataset.categoryid,
|
||||
tooltip: getString('newchild', 'qbank_managecategories', categoryItem.dataset.categoryname)
|
||||
}
|
||||
);
|
||||
const activityNameArea = categoryItem.querySelector(this.selectors.CATEGORY_DETAILS);
|
||||
await Templates.appendNodeContents(activityNameArea, html, js);
|
||||
// Reinstate padding on the parent element.
|
||||
this.element.closest(this.selectors.CATEGORY_CONTENTS).classList.remove(this.classes.NO_BOTTOM_PADDING);
|
||||
// Remove this list.
|
||||
this.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If a newly-created category has this list's category as its parent, add it to this list.
|
||||
*
|
||||
* @param {Object} args
|
||||
* @param {Object} args.element
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async addCategory({element}) {
|
||||
if (element.parent !== this.getElement().dataset.categoryid) {
|
||||
return; // Not for me.
|
||||
}
|
||||
const {html, js} = await Templates.renderForPromise('qbank_managecategories/category', element.templatecontext);
|
||||
Templates.appendNodeContents(this.getElement(), html, js);
|
||||
// If one of the children has no draghandle, it should do now it has a sibling.
|
||||
const noDragHandle = this.getElement(this.selectors.CATEGORY_NO_DRAGHANDLE);
|
||||
if (noDragHandle) {
|
||||
this.reactive.dispatch('showDragHandle', noDragHandle.dataset.categoryid);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Reactive module for category manager
|
||||
*
|
||||
* @module qbank_managecategories/categorymanager
|
||||
*/
|
||||
|
||||
import {Reactive} from 'core/reactive';
|
||||
import {get_string as getString} from 'core/str';
|
||||
import {mutations} from 'qbank_managecategories/mutations';
|
||||
import {eventTypes, notifyQbankManagecategoriesStateUpdated} from 'qbank_managecategories/events';
|
||||
import Ajax from "core/ajax";
|
||||
import Notification from "core/notification";
|
||||
import ModalForm from 'core_form/modalform';
|
||||
|
||||
const SELECTORS = {
|
||||
CATEGORY_LIST: '.qbank_managecategories-categorylist',
|
||||
CONTEXT: '.qbank_managecategories-categorylist[data-contextid]',
|
||||
CATEGORY_ITEM: '.qbank_managecategories-item[data-categoryid]',
|
||||
CATEGORY_ROOT: '#categoryroot',
|
||||
SHOWDESCRIPTIONS_TOGGLE: '#showdescriptions-toggle',
|
||||
ADD_EDIT_BUTTON: '[data-action="addeditcategory"]',
|
||||
};
|
||||
|
||||
const CLASSES = {
|
||||
DRAGHANDLE: 'draghandle',
|
||||
DANGER: 'alert-danger',
|
||||
};
|
||||
|
||||
/**
|
||||
* Load the initial state.
|
||||
*
|
||||
* This iterates over the initial tree of category items, and captures the data required for the state from each category.
|
||||
* It also captures a count of the number of children in each list.
|
||||
*
|
||||
* @param {Reactive} reactive
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
const loadState = async(reactive) => {
|
||||
const rootElement = document.querySelector(SELECTORS.CATEGORY_ROOT);
|
||||
const stateData = {
|
||||
page: {
|
||||
contextid: rootElement.dataset.contextid,
|
||||
showdescriptions: document.querySelector(SELECTORS.SHOWDESCRIPTIONS_TOGGLE).checked,
|
||||
},
|
||||
categories: [],
|
||||
categoryLists: [],
|
||||
};
|
||||
const listItems = document.querySelectorAll(SELECTORS.CATEGORY_ITEM);
|
||||
listItems.forEach(item => {
|
||||
stateData.categories.push({
|
||||
id: item.dataset.categoryid,
|
||||
name: item.dataset.categoryname,
|
||||
parent: item.dataset.parent,
|
||||
contextid: item.dataset.contextid,
|
||||
sortorder: item.dataset.sortorder,
|
||||
draghandle: item.classList.contains(CLASSES.DRAGHANDLE),
|
||||
});
|
||||
});
|
||||
const categoryLists = document.querySelectorAll(SELECTORS.CATEGORY_LIST);
|
||||
categoryLists.forEach(categoryList => {
|
||||
stateData.categoryLists.push({
|
||||
id: categoryList.dataset.categoryid,
|
||||
childCount: categoryList.querySelectorAll(SELECTORS.CATEGORY_ITEM).length,
|
||||
});
|
||||
});
|
||||
reactive.setInitialState(stateData);
|
||||
};
|
||||
|
||||
/**
|
||||
* Reactive instance for the category manager.
|
||||
*/
|
||||
class CategoryManager extends Reactive {
|
||||
/**
|
||||
* Move a category to a new position within the given parent.
|
||||
*
|
||||
* This will call the move_category web service function to re-order the categories, then update
|
||||
* the state with the returned updates.
|
||||
*
|
||||
* @param {Number} categoryId The ID of the category being moved.
|
||||
* @param {Number} targetParentId The ID of the destination parent category (this may not have changed).
|
||||
* @param {Number} precedingSiblingId The ID of the category to put the moved category after.
|
||||
* This may be null if moving to the top of a list.
|
||||
*/
|
||||
moveCategory(
|
||||
categoryId,
|
||||
targetParentId,
|
||||
precedingSiblingId = null,
|
||||
) {
|
||||
const call = {
|
||||
methodname: 'qbank_managecategories_move_category',
|
||||
args: {
|
||||
pagecontextid: this.state.page.contextid,
|
||||
categoryid: categoryId,
|
||||
targetparentid: targetParentId,
|
||||
precedingsiblingid: precedingSiblingId,
|
||||
}
|
||||
};
|
||||
Ajax.call([call])[0]
|
||||
.then((stateUpdates) => {
|
||||
this.stateManager.processUpdates(stateUpdates);
|
||||
return stateUpdates;
|
||||
})
|
||||
.catch(error => {
|
||||
Notification.addNotification({
|
||||
message: error.message,
|
||||
type: 'error',
|
||||
});
|
||||
document.getElementsByClassName(CLASSES.DANGER)[0]?.scrollIntoView();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return title for the add/edit modal.
|
||||
*
|
||||
* @param {boolean} isEdit is 'add' or 'edit' form
|
||||
* @returns {String} title string
|
||||
*/
|
||||
getTitle(isEdit) {
|
||||
return getString(isEdit ? 'editcategory' : 'addcategory', 'question');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return save button label for the add/edit modal.
|
||||
*
|
||||
* @param {boolean} isEdit is 'add' or 'edit' form
|
||||
* @returns {String} save string
|
||||
*/
|
||||
getSave(isEdit) {
|
||||
return isEdit ? getString('savechanges', 'core') : getString('addcategory', 'question');
|
||||
}
|
||||
|
||||
/**
|
||||
* Function handling display of modal form.
|
||||
*
|
||||
* @param {Event} e The click event triggering the modal.
|
||||
*/
|
||||
showEditModal(e) {
|
||||
const addEditButton = e.target.closest(SELECTORS.ADD_EDIT_BUTTON);
|
||||
|
||||
// Return if it is not 'addeditcategory' button.
|
||||
if (!addEditButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Return if the action type is not specified.
|
||||
if (!addEditButton.dataset.actiontype) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
// Data for the modal.
|
||||
const title = categorymanager.getTitle(addEditButton.dataset.actiontype === 'edit');
|
||||
const save = categorymanager.getSave(addEditButton.dataset.actiontype === 'edit');
|
||||
const cmid = addEditButton.dataset.cmid;
|
||||
const courseid = addEditButton.dataset.courseid;
|
||||
const questioncount = addEditButton.dataset.questioncount;
|
||||
let contextid = addEditButton.dataset.contextid;
|
||||
let categoryid = null;
|
||||
let sortorder = null;
|
||||
let parent = null;
|
||||
const categoryItem = e.target.closest(SELECTORS.CATEGORY_ITEM);
|
||||
if (categoryItem) {
|
||||
contextid = categoryItem.dataset.contextid;
|
||||
categoryid = categoryItem.dataset.categoryid;
|
||||
sortorder = categoryItem.dataset.sortorder;
|
||||
const parentContext = categoryItem.closest(SELECTORS.CONTEXT);
|
||||
parent = categoryItem.dataset.parent + ',' + parentContext.dataset.contextid;
|
||||
}
|
||||
|
||||
// Call the modal.
|
||||
const modalForm = new ModalForm({
|
||||
formClass: "qbank_managecategories\\form\\question_category_edit_form",
|
||||
args: {
|
||||
cmid,
|
||||
courseid,
|
||||
questioncount,
|
||||
contextid,
|
||||
categoryid,
|
||||
sortorder,
|
||||
parent,
|
||||
},
|
||||
modalConfig: {
|
||||
title: title,
|
||||
large: true,
|
||||
},
|
||||
saveButtonText: save,
|
||||
returnFocus: addEditButton,
|
||||
});
|
||||
// Once the form has been submitted via the web service, update the state with the new or updated
|
||||
// category based on the web service response.
|
||||
modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, (response) => {
|
||||
categorymanager.stateManager.processUpdates(response.detail);
|
||||
});
|
||||
// Show the form.
|
||||
modalForm.show();
|
||||
}
|
||||
}
|
||||
|
||||
export const categorymanager = new CategoryManager({
|
||||
name: 'qtype_managecategories_categorymanager',
|
||||
eventName: eventTypes.qbankManagecategoriesStateUpdated,
|
||||
eventDispatch: notifyQbankManagecategoriesStateUpdated,
|
||||
mutations,
|
||||
});
|
||||
|
||||
/**
|
||||
* Load the initial state.
|
||||
*/
|
||||
export const init = () => {
|
||||
loadState(categorymanager);
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* The category root component.
|
||||
*
|
||||
* @module qbank_managecategories/categoryroot
|
||||
* @class qbank_managecategories/categoryroot
|
||||
*/
|
||||
|
||||
import {BaseComponent} from 'core/reactive';
|
||||
import {categorymanager} from 'qbank_managecategories/categorymanager';
|
||||
|
||||
export default class extends BaseComponent {
|
||||
|
||||
create(descriptor) {
|
||||
this.name = descriptor.element.id;
|
||||
this.classes = {
|
||||
SHOWDESCRIPTIONS: 'showdescriptions',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Static method to create a component instance.
|
||||
*
|
||||
* @param {string} target the DOM main element or its ID
|
||||
* @param {object} selectors optional css selector overrides
|
||||
* @return {Component}
|
||||
*/
|
||||
static init(target, selectors) {
|
||||
return new this({
|
||||
element: document.querySelector(target),
|
||||
selectors,
|
||||
reactive: categorymanager,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch for changes to the page state.
|
||||
*
|
||||
* @return {Array} A list of watchers.
|
||||
*/
|
||||
getWatchers() {
|
||||
return [
|
||||
// Watch for descriptions being toggled.
|
||||
{watch: `page.showdescriptions:updated`, handler: this.toggleDescriptions}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Show or hide descriptions when the flag in the state is changed.
|
||||
*
|
||||
* @param {Object} args
|
||||
* @param {Object} args.element The updated page state.
|
||||
*/
|
||||
toggleDescriptions({element}) {
|
||||
if (element.showdescriptions) {
|
||||
this.getElement().classList.add(this.classes.SHOWDESCRIPTIONS);
|
||||
} else {
|
||||
this.getElement().classList.remove(this.classes.SHOWDESCRIPTIONS);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
import {dispatchEvent} from 'core/event_dispatcher';
|
||||
|
||||
/**
|
||||
* Reactive events for qbank_managecategories
|
||||
*
|
||||
* @module qbank_managecategories/events
|
||||
*
|
||||
*/
|
||||
export const eventTypes = {
|
||||
/**
|
||||
* Event triggered when the activity reactive state is updated.
|
||||
*
|
||||
* @event qbankManagecategoriesStateUpdated
|
||||
* @type {CustomEvent}
|
||||
* @property {Array} nodes The list of parent nodes which were updated
|
||||
*/
|
||||
qbankManagecategoriesStateUpdated: 'qbank_managecategories/stateUpdated',
|
||||
};
|
||||
|
||||
/**
|
||||
* Trigger an event to indicate that the activity state is updated.
|
||||
*
|
||||
* @method notifyQbankManagecategoriesStateUpdated
|
||||
* @param {Object} detail the full state
|
||||
* @param {HTMLElement} container the custom event target (document if none provided)
|
||||
* @returns {CustomEvent}
|
||||
* @fires qbankManagecategoriesStateUpdated
|
||||
*/
|
||||
export const notifyQbankManagecategoriesStateUpdated = (detail, container) => {
|
||||
return dispatchEvent(eventTypes.qbankManagecategoriesStateUpdated, detail, container);
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import {setUserPreference} from 'core_user/repository';
|
||||
|
||||
/**
|
||||
* Default mutation manager
|
||||
*
|
||||
* @module qbank_managecategories/mutations
|
||||
*/
|
||||
class Mutations {
|
||||
/**
|
||||
* Set the showdescriptions property to true or false in the page's state.
|
||||
*
|
||||
* @param {stateManager} stateManager
|
||||
* @param {boolean} showDescriptions
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async toggleDescriptions(stateManager, showDescriptions) {
|
||||
stateManager.setReadOnly(false);
|
||||
await setUserPreference('qbank_managecategories_showdescriptions', showDescriptions);
|
||||
stateManager.state.page.showdescriptions = showDescriptions;
|
||||
stateManager.setReadOnly(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the draghandle property to true in a given category's state.
|
||||
*
|
||||
* @param {stateManager} stateManager
|
||||
* @param {Number} categoryId
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async showDragHandle(stateManager, categoryId) {
|
||||
stateManager.setReadOnly(false);
|
||||
stateManager.state.categories.get(categoryId).draghandle = true;
|
||||
stateManager.setReadOnly(true);
|
||||
}
|
||||
}
|
||||
|
||||
export const mutations = new Mutations();
|
||||
@@ -0,0 +1,139 @@
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* The newchild component.
|
||||
*
|
||||
* This is a drop target for moving a category to an as-yet-nonexistant child list under another category.
|
||||
*
|
||||
* @module qbank_managecategories/newchild
|
||||
* @class qbank_managecategories/newchild
|
||||
*/
|
||||
|
||||
import {BaseComponent, DragDrop} from 'core/reactive';
|
||||
import $ from 'jquery';
|
||||
import {categorymanager} from 'qbank_managecategories/categorymanager';
|
||||
|
||||
export default class extends BaseComponent {
|
||||
create(descriptor) {
|
||||
this.name = descriptor.element.id;
|
||||
this.selectors = {
|
||||
NEW_CHILD: '.qbank_managecategories-newchild',
|
||||
CATEGORY_ID: id => `#category-${id}`
|
||||
};
|
||||
this.classes = {
|
||||
DROP_TARGET: 'qbank_managecategories-droptarget',
|
||||
};
|
||||
this.ids = {
|
||||
CATEGORY: id => `category-${id}`,
|
||||
};
|
||||
}
|
||||
|
||||
stateReady() {
|
||||
this.dragdrop = new DragDrop(this);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
// The draggable element must be unregistered.
|
||||
if (this.dragdrop !== undefined) {
|
||||
this.dragdrop.unregister();
|
||||
this.dragdrop = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Static method to create a component instance form the mustache template.
|
||||
*
|
||||
* @param {string} target the DOM main element or its ID
|
||||
* @param {object} selectors optional css selector overrides
|
||||
* @return {Component}
|
||||
*/
|
||||
static init(target, selectors) {
|
||||
const targetElement = document.querySelector(target);
|
||||
return new this({
|
||||
element: targetElement,
|
||||
selectors,
|
||||
reactive: categorymanager,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cannot drop a category as a new child of its own descendant.
|
||||
*
|
||||
* @param {Object} dropData
|
||||
* @return {boolean}
|
||||
*/
|
||||
validateDropData(dropData) {
|
||||
if (this.getElement().closest(this.selectors.CATEGORY_ID(dropData.id))) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
showDropZone(dropData, event) {
|
||||
const dropTarget = event.target.closest(this.selectors.NEW_CHILD);
|
||||
dropTarget.classList.add(this.classes.DROP_TARGET);
|
||||
$(dropTarget).tooltip('show');
|
||||
}
|
||||
|
||||
hideDropZone(dropData, event) {
|
||||
const dropTarget = event.target.closest(this.selectors.NEW_CHILD);
|
||||
dropTarget.classList.remove(this.classes.DROP_TARGET);
|
||||
$(dropTarget).tooltip('hide');
|
||||
}
|
||||
|
||||
drop(dropData, event) {
|
||||
const dropTarget = event.target.closest(this.selectors.NEW_CHILD);
|
||||
|
||||
if (!dropTarget) {
|
||||
return;
|
||||
}
|
||||
|
||||
const source = document.getElementById(this.ids.CATEGORY(dropData.id));
|
||||
|
||||
if (!source) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetParentId = dropTarget.dataset.parent;
|
||||
|
||||
// Insert the category as the first child of the new parent.
|
||||
categorymanager.moveCategory(dropData.id, targetParentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch for categories moving to a new parent.
|
||||
*
|
||||
* @return {Array} A list of watchers.
|
||||
*/
|
||||
getWatchers() {
|
||||
return [
|
||||
// Watch for any category having its parent changed.
|
||||
{watch: `categories.parent:updated`, handler: this.checkNewChild},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* If an element now has this category as the parent, remove this new child target.
|
||||
*
|
||||
* @param {Object} args
|
||||
* @param {Element} args.element
|
||||
*/
|
||||
checkNewChild({element}) {
|
||||
if (element.parent === parseInt(this.element.dataset.parent)) {
|
||||
this.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* The show descriptions toggle component.
|
||||
*
|
||||
* @module qbank_managecategories/showdescriptions
|
||||
* @class qbank_managecategories/showdescriptions
|
||||
*/
|
||||
|
||||
import {BaseComponent} from 'core/reactive';
|
||||
import {categorymanager} from 'qbank_managecategories/categorymanager';
|
||||
|
||||
export default class extends BaseComponent {
|
||||
|
||||
create(descriptor) {
|
||||
this.name = descriptor.element.id;
|
||||
this.selectors = {
|
||||
TOGGLE: '#showdescriptions-toggle',
|
||||
};
|
||||
}
|
||||
|
||||
stateReady() {
|
||||
this.addEventListener(this.getElement(this.selectors.TOGGLE), 'change', this.updateShowDescriptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Static method to create a component instance.
|
||||
*
|
||||
* @param {string} target the DOM main element or its ID
|
||||
* @param {object} selectors optional css selector overrides
|
||||
* @return {Component}
|
||||
*/
|
||||
static init(target, selectors) {
|
||||
const targetElement = document.querySelector(target);
|
||||
return new this({
|
||||
element: targetElement,
|
||||
selectors,
|
||||
reactive: categorymanager,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a mutation to toggle the showDescriptions setting.
|
||||
*
|
||||
* @param {Event} event The toggle change event.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async updateShowDescriptions(event) {
|
||||
const checked = event.target.checked;
|
||||
this.reactive.dispatch('toggleDescriptions', checked);
|
||||
}
|
||||
}
|
||||
@@ -24,101 +24,62 @@
|
||||
*/
|
||||
|
||||
require_once(__DIR__ . '/../../../config.php');
|
||||
require_once($CFG->dirroot."/question/editlib.php");
|
||||
require_once($CFG->dirroot . '/question/editlib.php');
|
||||
|
||||
use core_question\output\qbank_actionbar;
|
||||
use core_question\category_manager;
|
||||
use qbank_managecategories\form\question_move_form;
|
||||
use qbank_managecategories\helper;
|
||||
use qbank_managecategories\question_category_object;
|
||||
use qbank_managecategories\output\categories;
|
||||
use qbank_managecategories\output\categories_header;
|
||||
use qbank_managecategories\question_categories;
|
||||
|
||||
require_login();
|
||||
core_question\local\bank\helper::require_plugin_enabled(helper::PLUGINNAME);
|
||||
|
||||
list($thispageurl, $contexts, $cmid, $cm, $module, $pagevars) =
|
||||
question_edit_setup('categories', '/question/bank/managecategories/category.php');
|
||||
question_edit_setup('categories', '/question/bank/managecategories/category.php');
|
||||
$courseid = optional_param('courseid', 0, PARAM_INT);
|
||||
|
||||
// Get values from form for actions on this page.
|
||||
$param = new stdClass();
|
||||
$param->moveup = optional_param('moveup', 0, PARAM_INT);
|
||||
$param->movedown = optional_param('movedown', 0, PARAM_INT);
|
||||
$param->moveupcontext = optional_param('moveupcontext', 0, PARAM_INT);
|
||||
$param->movedowncontext = optional_param('movedowncontext', 0, PARAM_INT);
|
||||
$param->tocontext = optional_param('tocontext', 0, PARAM_INT);
|
||||
$param->left = optional_param('left', 0, PARAM_INT);
|
||||
$param->right = optional_param('right', 0, PARAM_INT);
|
||||
$param->delete = optional_param('delete', 0, PARAM_INT);
|
||||
$param->confirm = optional_param('confirm', 0, PARAM_INT);
|
||||
$param->cancel = optional_param('cancel', '', PARAM_ALPHA);
|
||||
$param->move = optional_param('move', 0, PARAM_INT);
|
||||
$param->moveto = optional_param('moveto', 0, PARAM_INT);
|
||||
$param->edit = optional_param('edit', null, PARAM_INT);
|
||||
|
||||
$url = new moodle_url($thispageurl);
|
||||
foreach ((array)$param as $key => $value) {
|
||||
if (($key !== 'cancel' && $key !== 'edit' && $value !== 0) ||
|
||||
($key === 'cancel' && $value !== '') ||
|
||||
($key === 'edit' && $value !== null)) {
|
||||
$url->param($key, $value);
|
||||
}
|
||||
}
|
||||
$PAGE->set_url($url);
|
||||
|
||||
$qcobject = new question_category_object($pagevars['cpage'], $thispageurl,
|
||||
$contexts->having_one_edit_tab_cap('categories'), $param->edit,
|
||||
$pagevars['cat'], $param->delete, $contexts->having_cap('moodle/question:add'));
|
||||
|
||||
if ($param->left || $param->right || $param->moveup || $param->movedown) {
|
||||
require_sesskey();
|
||||
|
||||
foreach ($qcobject->editlists as $list) {
|
||||
// Processing of these actions is handled in the method where appropriate and page redirects.
|
||||
$list->process_actions($param->left, $param->right, $param->moveup, $param->movedown);
|
||||
}
|
||||
if (!is_null($cmid)) {
|
||||
$thiscontext = context_module::instance($cmid)->id;
|
||||
} else {
|
||||
$course = get_course($courseid);
|
||||
$thiscontext = context_course::instance($course->id)->id;
|
||||
}
|
||||
|
||||
if ($param->moveupcontext || $param->movedowncontext) {
|
||||
require_sesskey();
|
||||
$todelete = optional_param('delete', 0, PARAM_INT); // The ID of a category to delete.
|
||||
|
||||
if ($param->moveupcontext) {
|
||||
$catid = $param->moveupcontext;
|
||||
} else {
|
||||
$catid = $param->movedowncontext;
|
||||
}
|
||||
$newtopcat = question_get_top_category($param->tocontext);
|
||||
if (!$newtopcat) {
|
||||
throw new moodle_exception('invalidcontext');
|
||||
}
|
||||
$oldcat = $DB->get_record('question_categories', ['id' => $catid], '*', MUST_EXIST);
|
||||
// Log the move to another context.
|
||||
$category = new stdClass();
|
||||
$category->id = explode(',', $pagevars['cat'], -1)[0];
|
||||
$category->contextid = $param->tocontext;
|
||||
$event = \core\event\question_category_moved::create_from_question_category_instance($category);
|
||||
$event->trigger();
|
||||
// Update the set_reference records when moving a category to a different context.
|
||||
move_question_set_references($catid, $catid, $oldcat->contextid, $category->contextid);
|
||||
$qcobject->update_category($catid, "{$newtopcat->id},{$param->tocontext}", $oldcat->name, $oldcat->info);
|
||||
// The previous line does a redirect().
|
||||
}
|
||||
$PAGE->set_url($thispageurl);
|
||||
$PAGE->add_body_class('limitedwidth');
|
||||
|
||||
if ($param->delete) {
|
||||
if (!$category = $DB->get_record("question_categories", ["id" => $param->delete])) {
|
||||
throw new moodle_exception('nocate', 'question', $thispageurl->out(), $param->delete);
|
||||
$manager = new category_manager($thispageurl);
|
||||
|
||||
if ($todelete) {
|
||||
if (!$category = $DB->get_record("question_categories", ["id" => $todelete])) {
|
||||
throw new moodle_exception('nocate', 'question', $thispageurl->out(), $todelete);
|
||||
}
|
||||
|
||||
helper::question_remove_stale_questions_from_category($param->delete);
|
||||
helper::question_remove_stale_questions_from_category($todelete);
|
||||
|
||||
$questionstomove = count($qcobject->get_real_question_ids_in_category($param->delete));
|
||||
$questionstomove = count($manager->get_real_question_ids_in_category($todelete));
|
||||
|
||||
// Second pass, if we still have questions to move, setup the form.
|
||||
if ($questionstomove) {
|
||||
$categorycontext = context::instance_by_id($category->contextid);
|
||||
$moveform = new question_move_form($thispageurl,
|
||||
['contexts' => [$categorycontext], 'currentcat' => $param->delete]);
|
||||
['contexts' => [$categorycontext], 'currentcat' => $todelete]);
|
||||
if ($moveform->is_cancelled()) {
|
||||
$thispageurl->remove_all_params();
|
||||
if (!is_null($cmid)) {
|
||||
$thispageurl->param('cmid', $cmid);
|
||||
} else {
|
||||
$thispageurl->param('courseid', $courseid);
|
||||
}
|
||||
redirect($thispageurl);
|
||||
} else if ($formdata = $moveform->get_data()) {
|
||||
list($tocategoryid, $tocontextid) = explode(',', $formdata->category);
|
||||
$qcobject->move_questions_and_delete_category($formdata->delete, $tocategoryid);
|
||||
$manager->move_questions_and_delete_category($formdata->delete, $tocategoryid);
|
||||
$thispageurl->remove_params('cat', 'category');
|
||||
redirect($thispageurl);
|
||||
}
|
||||
@@ -127,54 +88,24 @@ if ($param->delete) {
|
||||
$questionstomove = 0;
|
||||
}
|
||||
|
||||
if ($qcobject->catform->is_cancelled()) {
|
||||
redirect($thispageurl);
|
||||
} else if ($catformdata = $qcobject->catform->get_data()) {
|
||||
$catformdata->infoformat = $catformdata->info['format'];
|
||||
$catformdata->info = $catformdata->info['text'];
|
||||
if (!$catformdata->id) {// New category.
|
||||
$qcobject->add_category($catformdata->parent, $catformdata->name,
|
||||
$catformdata->info, false, $catformdata->infoformat, $catformdata->idnumber);
|
||||
} else {
|
||||
$qcobject->update_category($catformdata->id, $catformdata->parent,
|
||||
$catformdata->name, $catformdata->info, $catformdata->infoformat, $catformdata->idnumber);
|
||||
}
|
||||
redirect($thispageurl);
|
||||
} else if ((!empty($param->delete) and (!$questionstomove) and confirm_sesskey())) {
|
||||
$qcobject->delete_category($param->delete);// Delete the category now no questions to move.
|
||||
if ((!empty($todelete) && (!$questionstomove) && confirm_sesskey())) {
|
||||
$manager->delete_category($todelete);// Delete the category now no questions to move.
|
||||
$thispageurl->remove_params('cat', 'category');
|
||||
redirect($thispageurl);
|
||||
}
|
||||
|
||||
if ($param->edit !== null || $qcobject->catform->is_submitted()) {
|
||||
// In the is_submitted case, we only get here if it was submitted,
|
||||
// but not valid, so we need to show the validation error.
|
||||
$PAGE->navbar->add(get_string('editingcategory', 'question'));
|
||||
}
|
||||
|
||||
$PAGE->set_title(get_string('editcategories', 'question'));
|
||||
$PAGE->set_heading($COURSE->fullname);
|
||||
$PAGE->activityheader->disable();
|
||||
|
||||
echo $OUTPUT->header();
|
||||
|
||||
// Print horizontal nav if needed.
|
||||
$renderer = $PAGE->get_renderer('core_question', 'bank');
|
||||
|
||||
$qbankaction = new \core_question\output\qbank_action_menu($url);
|
||||
if (is_null($param->edit)) {
|
||||
$actionurl = new moodle_url($url, ['edit' => 0]);
|
||||
$qbankaction->set_action_button($actionurl, get_string('addcategory', 'question'));
|
||||
}
|
||||
$categoriesrenderer = $PAGE->get_renderer('qbank_managecategories');
|
||||
echo $OUTPUT->header();
|
||||
$qbankaction = new \core_question\output\qbank_action_menu($thispageurl);
|
||||
echo $renderer->render($qbankaction);
|
||||
|
||||
// Display the UI.
|
||||
if ($param->edit !== null || $qcobject->catform->is_submitted()) {
|
||||
// In the is_submitted case, we only get here if it was submitted,
|
||||
// but not valid, so we need to show the validation error.
|
||||
// In this case, category id is in the 'id' hidden filed.
|
||||
$qcobject->edit_single_category($param->edit ?? required_param('id', PARAM_INT));
|
||||
} else if ($questionstomove) {
|
||||
if ($questionstomove) {
|
||||
$vars = new stdClass();
|
||||
$vars->name = $category->name;
|
||||
$vars->count = $questionstomove;
|
||||
@@ -182,6 +113,16 @@ if ($param->edit !== null || $qcobject->catform->is_submitted()) {
|
||||
$moveform->display();
|
||||
} else {
|
||||
// Display the user interface.
|
||||
$qcobject->display_user_interface();
|
||||
$questioncategories = new question_categories(
|
||||
$thispageurl,
|
||||
$contexts->having_one_edit_tab_cap('categories'),
|
||||
$cmid,
|
||||
$courseid,
|
||||
$thiscontext,
|
||||
);
|
||||
$PAGE->requires->js_call_amd('qbank_managecategories/categorymanager', 'init'); // Load reactive module.
|
||||
echo $OUTPUT->render(new categories_header($questioncategories));
|
||||
echo $OUTPUT->render(new categories($questioncategories));
|
||||
}
|
||||
|
||||
echo $OUTPUT->footer();
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace qbank_managecategories\external;
|
||||
|
||||
use core_external\external_api;
|
||||
use core_external\external_function_parameters;
|
||||
use core_external\external_value;
|
||||
use core_external\external_single_structure;
|
||||
use core_external\external_multiple_structure;
|
||||
use core_question\category_manager;
|
||||
use moodle_exception;
|
||||
use context;
|
||||
use qbank_managecategories\helper;
|
||||
|
||||
/**
|
||||
* External class used for category reordering.
|
||||
*
|
||||
* @package qbank_managecategories
|
||||
* @category external
|
||||
* @copyright 2024 Catalyst IT Europe Ltd.
|
||||
* @author Mark Johnson <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class move_category extends external_api {
|
||||
/**
|
||||
* Describes the parameters for update_category_order webservice.
|
||||
* @return external_function_parameters
|
||||
*/
|
||||
public static function execute_parameters(): external_function_parameters {
|
||||
return new external_function_parameters([
|
||||
'pagecontextid' => new external_value(PARAM_INT, 'The context of the current page'),
|
||||
'categoryid' => new external_value(PARAM_INT, 'Category being moved'),
|
||||
'targetparentid' => new external_value(PARAM_INT, 'The ID of the parent category to move to.'),
|
||||
'precedingsiblingid' => new external_value(
|
||||
PARAM_INT,
|
||||
'The ID of the preceding category. Null if this is being moved to top of its parent',
|
||||
allownull: NULL_ALLOWED,
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns description of method result value.
|
||||
*
|
||||
* This function will always return a set of state updates for the core/reactive state.
|
||||
* {@link https://moodledev.io/docs/4.2/guides/javascript/reactive#controlling-the-state-from-the-backend}
|
||||
*
|
||||
* @return external_multiple_structure
|
||||
*/
|
||||
public static function execute_returns(): external_multiple_structure {
|
||||
return new external_multiple_structure(
|
||||
new external_single_structure(
|
||||
[
|
||||
'name' => new external_value(PARAM_ALPHA, 'State object name (always "categories" from this function).'),
|
||||
'action' => new external_value(PARAM_ALPHA, 'State update type (always "put" from this function).'),
|
||||
'fields' => new external_single_structure(
|
||||
[
|
||||
'id' => new external_value(PARAM_INT, 'The ID of the category that was updated.'),
|
||||
'sortorder' => new external_value(PARAM_INT, 'The new sortorder', VALUE_OPTIONAL),
|
||||
'parent' => new external_value(PARAM_INT, 'The ID of the new parent category.', VALUE_OPTIONAL),
|
||||
'context' => new external_value(PARAM_INT, 'The ID of the new context.', VALUE_OPTIONAL),
|
||||
'draghandle' => new external_value(
|
||||
PARAM_BOOL,
|
||||
'Should this category have a drag handle?',
|
||||
VALUE_OPTIONAL
|
||||
),
|
||||
]
|
||||
),
|
||||
],
|
||||
'An individual state update',
|
||||
),
|
||||
'Category state updates',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move category to new location.
|
||||
*
|
||||
* @param int $pagecontextid ID of the context of the current page.
|
||||
* @param int $categoryid ID of the category to move.
|
||||
* @param int $targetparentid The ID of the parent category to move to.
|
||||
* @param ?int $precedingsiblingid The ID of the preceding category. Null if this is being moved to top of its parent.
|
||||
* @return array Reactive state updates representing the changes made to the categories.
|
||||
*/
|
||||
public static function execute(
|
||||
int $pagecontextid,
|
||||
int $categoryid,
|
||||
int $targetparentid,
|
||||
?int $precedingsiblingid = null
|
||||
): array {
|
||||
// Update category location.
|
||||
global $DB, $CFG;
|
||||
|
||||
require_once($CFG->libdir . '/questionlib.php');
|
||||
|
||||
$context = context::instance_by_id($pagecontextid);
|
||||
self::validate_context($context);
|
||||
$manager = new category_manager();
|
||||
|
||||
$origincategory = $DB->get_record('question_categories', ['id' => $categoryid], '*', MUST_EXIST);
|
||||
$targetparent = $DB->get_record('question_categories', ['id' => $targetparentid], '*', MUST_EXIST);
|
||||
if ($precedingsiblingid) {
|
||||
$precedingsibling = $DB->get_record('question_categories', ['id' => $precedingsiblingid], '*', MUST_EXIST);
|
||||
}
|
||||
|
||||
// Check permission for original and destination contexts.
|
||||
$manager->require_manage_category(context::instance_by_id($origincategory->contextid));
|
||||
|
||||
if ($origincategory->contextid != $targetparent->contextid) {
|
||||
$manager->require_manage_category(context::instance_by_id($targetparent->contextid));
|
||||
}
|
||||
|
||||
$originstateupdate = self::make_state_update($origincategory->id);
|
||||
$stateupdates = [];
|
||||
|
||||
$transaction = $DB->start_delegated_transaction();
|
||||
|
||||
// Set new parent.
|
||||
if ($origincategory->parent !== $targetparent->id) {
|
||||
$newsiblings = $DB->get_fieldset('question_categories', 'id', ['parent' => $targetparent->id]);
|
||||
if (
|
||||
count($newsiblings) == 1
|
||||
&& $manager->is_only_child_of_top_category_in_context(reset($newsiblings))
|
||||
) {
|
||||
// If we are moving to a top-level parent that only had 1 category before, allow re-ordering of that category.
|
||||
$stateupdates[] = self::make_state_update(reset($newsiblings), draghandle: true);
|
||||
}
|
||||
$originstateupdate->fields->parent = $targetparent->id;
|
||||
}
|
||||
|
||||
// Change to the same context.
|
||||
if ($origincategory->contextid !== $targetparent->contextid) {
|
||||
// Check for duplicate idnumber.
|
||||
if (
|
||||
!is_null($origincategory->idnumber)
|
||||
&& !$manager->idnumber_is_unique_in_context($origincategory->idnumber, $targetparent->contextid)
|
||||
) {
|
||||
throw new moodle_exception('idnumberexists', 'qbank_managecategories');
|
||||
}
|
||||
$originstateupdate->fields->context = $targetparent->contextid;
|
||||
}
|
||||
|
||||
// Update sort order.
|
||||
if ($precedingsiblingid) {
|
||||
$sortorder = $precedingsibling->sortorder + 1;
|
||||
} else {
|
||||
$sortorder = 1;
|
||||
}
|
||||
$originstateupdate->fields->sortorder = $sortorder;
|
||||
|
||||
// Save the updated parent, context and sortorder.
|
||||
$manager->update_category(
|
||||
$categoryid,
|
||||
helper::combine_id_context($targetparent),
|
||||
$origincategory->name,
|
||||
$origincategory->info,
|
||||
$origincategory->infoformat,
|
||||
$origincategory->idnumber,
|
||||
$sortorder,
|
||||
);
|
||||
|
||||
// Get other categories which are after the new position, and update their sortorder.
|
||||
$params = [
|
||||
'parent' => $targetparent->id,
|
||||
'sortorder' => $sortorder,
|
||||
'origincategoryid' => $origincategory->id,
|
||||
];
|
||||
$select = "
|
||||
parent = :parent
|
||||
AND id <> :origincategoryid
|
||||
AND sortorder >= :sortorder";
|
||||
$sort = "sortorder ASC";
|
||||
$toupdatesortorder = $DB->get_records_select('question_categories', $select, $params, $sort);
|
||||
foreach ($toupdatesortorder as $category) {
|
||||
$DB->set_field('question_categories', 'sortorder', ++$sortorder, ['id' => $category->id]);
|
||||
$stateupdates[] = self::make_state_update($category->id, sortorder: $sortorder);
|
||||
}
|
||||
|
||||
if (isset($originstateupdate->fields->parent)) {
|
||||
// If the category has moved parent, re-order the original siblings to fill the gap.
|
||||
$originsortorder = 1;
|
||||
$params = [
|
||||
'parent' => $origincategory->parent,
|
||||
];
|
||||
$select = "parent = :parent";
|
||||
$sort = "sortorder ASC";
|
||||
$originsiblings = $DB->get_records_select('question_categories', $select, $params, $sort);
|
||||
if (
|
||||
count($originsiblings) == 1
|
||||
&& $manager->is_only_child_of_top_category_in_context(reset($originsiblings)->id)
|
||||
) {
|
||||
// If this is now the only category in the context, don't allow re-ordering.
|
||||
$stateupdates[] = self::make_state_update(
|
||||
reset($originsiblings)->id,
|
||||
sortorder: $originsortorder,
|
||||
draghandle: false,
|
||||
);
|
||||
} else {
|
||||
foreach ($originsiblings as $category) {
|
||||
if ($category->sortorder !== $originsortorder) {
|
||||
$DB->set_field('question_categories', 'sortorder', $originsortorder, ['id' => $category->id]);
|
||||
$stateupdates[] = self::make_state_update($category->id, sortorder: $originsortorder);
|
||||
}
|
||||
$originsortorder++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$transaction->allow_commit();
|
||||
|
||||
// Return the updated for the moved category, followed by any additional updates that happened as a result.
|
||||
array_unshift($stateupdates, $originstateupdate);
|
||||
|
||||
return $stateupdates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a category state update based on the provided fields.
|
||||
*
|
||||
* @param int $id Category ID, required.
|
||||
* @param int|null $sortorder New sortorder, optional.
|
||||
* @param int|null $parent Category ID of new parent, optional.
|
||||
* @param bool|null $draghandle Set display of the drag handle. Optional.
|
||||
* @return \stdClass The update object.
|
||||
*/
|
||||
protected static function make_state_update(
|
||||
int $id,
|
||||
?int $sortorder = null,
|
||||
?int $parent = null,
|
||||
?bool $draghandle = null,
|
||||
): \stdClass {
|
||||
$update = (object)[
|
||||
'name' => 'categories',
|
||||
'action' => 'put',
|
||||
'fields' => (object)[
|
||||
'id' => $id,
|
||||
],
|
||||
];
|
||||
if (!is_null($sortorder)) {
|
||||
$update->fields->sortorder = $sortorder;
|
||||
}
|
||||
if (!is_null($parent)) {
|
||||
$update->fields->parent = $parent;
|
||||
}
|
||||
if (!is_null($draghandle)) {
|
||||
$update->fields->draghandle = $draghandle;
|
||||
}
|
||||
return $update;
|
||||
}
|
||||
}
|
||||
@@ -16,12 +16,20 @@
|
||||
|
||||
namespace qbank_managecategories\form;
|
||||
|
||||
use moodleform;
|
||||
use context;
|
||||
use context_module;
|
||||
use context_course;
|
||||
use qbank_managecategories\helper;
|
||||
use moodle_exception;
|
||||
use moodle_url;
|
||||
use core_question\local\bank\question_edit_contexts;
|
||||
use qbank_managecategories\output\category;
|
||||
use core_question\category_manager;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once($CFG->libdir.'/formslib.php');
|
||||
require_once($CFG->libdir . '/formslib.php');
|
||||
require_once($CFG->libdir . '/questionlib.php');
|
||||
|
||||
/**
|
||||
* Defines the form for editing question categories.
|
||||
@@ -32,7 +40,7 @@ require_once($CFG->libdir.'/formslib.php');
|
||||
* @copyright 2007 Jamie Pratt [email protected]
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class question_category_edit_form extends moodleform {
|
||||
class question_category_edit_form extends \core_form\dynamic_form {
|
||||
/** @var ?category_manager $manager */
|
||||
protected ?category_manager $manager = null;
|
||||
|
||||
@@ -51,6 +59,39 @@ class question_category_edit_form extends moodleform {
|
||||
return $this->manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the category, contexts, course and cmid based on the data provided via AJAX.
|
||||
*
|
||||
* @return array
|
||||
* @throws \coding_exception
|
||||
*/
|
||||
protected function get_current_data() {
|
||||
// If categoryid is set, we are editing an existing category.
|
||||
$currentcat = isset($this->_ajaxformdata['categoryid']) ? (int)$this->_ajaxformdata['categoryid'] : 0;
|
||||
// Determine the context based on the provided IDs.
|
||||
$cmid = isset($this->_ajaxformdata['cmid']) ? (int)$this->_ajaxformdata['cmid'] : 0;
|
||||
$courseid = isset($this->_ajaxformdata['courseid']) ? (int)$this->_ajaxformdata['courseid'] : 0;
|
||||
if ($cmid !== 0) {
|
||||
$thiscontext = context_module::instance($cmid);
|
||||
}
|
||||
|
||||
if ($courseid !== 0) {
|
||||
$thiscontext = context_course::instance($courseid);
|
||||
}
|
||||
|
||||
if ($courseid === 0 && $cmid === 0) {
|
||||
$parentcontext = (int)explode(',', $this->_ajaxformdata['parent'])[1];
|
||||
$contextid = $parentcontext === 0 ? $this->_ajaxformdata['contextid'] : (int)$parentcontext;
|
||||
$thiscontext = context::instance_by_id($contextid);
|
||||
}
|
||||
|
||||
if ($thiscontext) {
|
||||
$contexts = new question_edit_contexts($thiscontext);
|
||||
$contexts = $contexts->all();
|
||||
}
|
||||
return [$currentcat, $cmid, $courseid, $thiscontext, $contexts];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the form definition.
|
||||
*
|
||||
@@ -60,13 +101,32 @@ class question_category_edit_form extends moodleform {
|
||||
protected function definition() {
|
||||
$mform = $this->_form;
|
||||
|
||||
$contexts = $this->_customdata['contexts'];
|
||||
$currentcat = $this->_customdata['currentcat'];
|
||||
[$currentcat, $cmid, $courseid, $thiscontext, $contexts] = $this->get_current_data();
|
||||
|
||||
$mform->addElement('header', 'categoryheader', get_string('addcategory', 'question'));
|
||||
$mform->addElement('hidden', 'contextid', $thiscontext->id);
|
||||
$mform->setType('contextid', PARAM_INT);
|
||||
|
||||
$mform->addElement('questioncategory', 'parent', get_string('parentcategory', 'question'),
|
||||
['contexts' => $contexts, 'top' => true, 'currentcat' => $currentcat, 'nochildrenof' => $currentcat]);
|
||||
$mform->addElement('hidden', 'courseid', $courseid);
|
||||
$mform->setType('courseid', PARAM_INT);
|
||||
|
||||
$mform->addElement('hidden', 'cmid', $cmid);
|
||||
$mform->setType('cmid', PARAM_INT);
|
||||
|
||||
$mform->addElement('hidden', 'currentparent');
|
||||
$mform->setDefault('currentparent', $this->_ajaxformdata['parent'] ?? '');
|
||||
$mform->setType('currentparent', PARAM_SEQUENCE);
|
||||
|
||||
$mform->addElement(
|
||||
'questioncategory',
|
||||
'parent',
|
||||
get_string('parentcategory', 'question'),
|
||||
[
|
||||
'contexts' => $contexts,
|
||||
'top' => true,
|
||||
'currentcat' => $currentcat,
|
||||
'nochildrenof' => $currentcat,
|
||||
],
|
||||
);
|
||||
$mform->setType('parent', PARAM_SEQUENCE);
|
||||
if ($this->get_manager()->is_only_child_of_top_category_in_context($currentcat)) {
|
||||
$mform->hardFreeze('parent');
|
||||
@@ -87,33 +147,22 @@ class question_category_edit_form extends moodleform {
|
||||
$mform->addHelpButton('idnumber', 'idnumber', 'question');
|
||||
$mform->setType('idnumber', PARAM_RAW);
|
||||
|
||||
$this->add_action_buttons(true, get_string('addcategory', 'question'));
|
||||
|
||||
$mform->addElement('hidden', 'id', 0);
|
||||
$mform->setType('id', PARAM_INT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set data method.
|
||||
*
|
||||
* Add additional information to current data.
|
||||
* @param \stdClass|array $current Object or array of default current data.
|
||||
*/
|
||||
public function set_data($current) {
|
||||
if (is_object($current)) {
|
||||
$current = (array) $current;
|
||||
}
|
||||
if (!empty($current['info'])) {
|
||||
$current['info'] = ['text' => $current['info'], 'infoformat' => $current['infoformat']];
|
||||
} else {
|
||||
$current['info'] = ['text' => '', 'infoformat' => FORMAT_HTML];
|
||||
}
|
||||
parent::set_data($current);
|
||||
$mform->addElement('hidden', 'questioncount', $this->_ajaxformdata['questioncount'] ?? 0);
|
||||
$mform->setType('questioncount', PARAM_INT);
|
||||
|
||||
$mform->addElement('hidden', 'sortorder', $this->_ajaxformdata['sortorder'] ?? 0);
|
||||
$mform->setType('sortorder', PARAM_INT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation.
|
||||
*
|
||||
* Ensure that we aren't trying to move the only child of a top category to a different context.
|
||||
* Ensure that the ID Number is unique within the target context.
|
||||
*
|
||||
* @param array $data
|
||||
* @param array $files
|
||||
* @return array the errors that were found
|
||||
@@ -123,21 +172,150 @@ class question_category_edit_form extends moodleform {
|
||||
global $DB;
|
||||
|
||||
$errors = parent::validation($data, $files);
|
||||
|
||||
$currentrec = $DB->get_record('question_categories', ['id' => $data['id']]);
|
||||
$currentparent = $data['currentparent'];
|
||||
$data['parent'] ??= $currentparent; // If no parent was submitted, use the current parent.
|
||||
[$parentid, $contextid] = explode(',', $data['parent']);
|
||||
if ($currentrec) {
|
||||
$currentparent = $currentrec->parent . ',' . $currentrec->contextid;
|
||||
// Cannot move the last category in a context to another parent.
|
||||
$lastcategoryinthiscontext = $this->get_manager()->is_only_child_of_top_category_in_context($data['id']);
|
||||
if ($lastcategoryinthiscontext && $currentparent !== $data['parent']) {
|
||||
if ($parentid !== $this->_ajaxformdata['id']) {
|
||||
$errors['parent'] = get_string('lastcategoryinthiscontext', 'qbank_managecategories');
|
||||
}
|
||||
}
|
||||
// Cannot move category in same category.
|
||||
if ($currentrec->id === $parentid && $currentrec->contextid === $contextid) {
|
||||
$errors['parent'] = get_string('categoryincategory', 'qbank_managecategories');
|
||||
}
|
||||
}
|
||||
// Add field validation check for duplicate idnumber.
|
||||
list($parentid, $contextid) = explode(',', $data['parent']);
|
||||
if (((string) $data['idnumber'] !== '') && !empty($contextid)) {
|
||||
$conditions = 'contextid = ? AND idnumber = ?';
|
||||
$params = [$contextid, $data['idnumber']];
|
||||
if (!empty($data['id'])) {
|
||||
$conditions .= ' AND id <> ?';
|
||||
$params[] = $data['id'];
|
||||
}
|
||||
if ($DB->record_exists_select('question_categories', $conditions, $params)) {
|
||||
$errors['idnumber'] = get_string('idnumbertaken', 'error');
|
||||
}
|
||||
$id = $data['id'] ?? null;
|
||||
if (!empty($data['idnumber']) && !$this->get_manager()->idnumber_is_unique_in_context($data['idnumber'], $contextid, $id)) {
|
||||
$errors['idnumber'] = get_string('idnumbertaken', 'error');
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function get_context_for_dynamic_submission(): context {
|
||||
$contextid = $this->optional_param('contextid', 0, PARAM_INT);
|
||||
if ($contextid === 0) {
|
||||
$contextid = $this->_ajaxformdata['contextid'];
|
||||
}
|
||||
return context::instance_by_id($contextid);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function check_access_for_dynamic_submission(): void {
|
||||
$this->get_manager()->require_manage_category($this->get_context_for_dynamic_submission());
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the category form
|
||||
*
|
||||
* Either add or update a question category, then return a reactive state update for the category.
|
||||
*
|
||||
* @return array[] Reactive state update.
|
||||
* {@link https://moodledev.io/docs/4.2/guides/javascript/reactive#controlling-the-state-from-the-backend}
|
||||
* @throws \coding_exception
|
||||
* @throws \dml_exception
|
||||
* @throws moodle_exception
|
||||
*/
|
||||
public function process_dynamic_submission(): array {
|
||||
global $DB, $OUTPUT;
|
||||
|
||||
$values = $this->get_data();
|
||||
|
||||
$newinfo = format_text($values->info['text'], (int)$values->info['format'], ['noclean' => false]);
|
||||
$idnumber = $values->idnumber;
|
||||
|
||||
if ((string)$idnumber === '') {
|
||||
$idnumber = null;
|
||||
}
|
||||
|
||||
if (empty($values->id)) {
|
||||
$values->id = $this->get_manager()->add_category(
|
||||
$values->parent,
|
||||
$values->name,
|
||||
$newinfo,
|
||||
(int)$values->info['format'],
|
||||
$idnumber,
|
||||
);
|
||||
} else {
|
||||
$this->get_manager()->update_category(
|
||||
$values->id,
|
||||
$values->parent ?? '', // If we can't change the parent, it is not passed through he form.
|
||||
$values->name,
|
||||
$newinfo,
|
||||
(int)$values->info['format'],
|
||||
$idnumber,
|
||||
);
|
||||
}
|
||||
|
||||
$record = $DB->get_record('question_categories', ['id' => $values->id]);
|
||||
|
||||
// The question count will never change, we just need it passed through to re-render the category.
|
||||
$record->questioncount = $values->questioncount;
|
||||
|
||||
$category = new category(
|
||||
$record,
|
||||
context::instance_by_id($record->contextid),
|
||||
$values->cmid ?? 0,
|
||||
$values->courseid ?? 0,
|
||||
);
|
||||
|
||||
return [
|
||||
[
|
||||
'name' => 'categories',
|
||||
'action' => 'put',
|
||||
'fields' => [
|
||||
'id' => $record->id,
|
||||
'name' => $record->name,
|
||||
'parent' => $record->parent,
|
||||
'sortorder' => $record->sortorder,
|
||||
'draghandle' => $category->get_canreorder(),
|
||||
'templatecontext' => $category->export_for_template($OUTPUT),
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function set_data_for_dynamic_submission(): void {
|
||||
$categoryid = isset($this->_ajaxformdata['categoryid']) ? (int)$this->_ajaxformdata['categoryid'] : 0;
|
||||
if ($categoryid !== 0) {
|
||||
global $DB;
|
||||
$cattoset = $DB->get_record('question_categories', ['id' => $categoryid]);
|
||||
$this->set_data((object)[
|
||||
'id' => (int)$cattoset->id,
|
||||
'name' => $cattoset->name,
|
||||
'contextid' => (int)$cattoset->contextid,
|
||||
'info' => [
|
||||
'format' => FORMAT_HTML,
|
||||
'text' => $cattoset->info,
|
||||
],
|
||||
'infoformat' => (int)$cattoset->infoformat,
|
||||
'parent' => (int)$cattoset->parent . ',' . (int)$cattoset->contextid,
|
||||
'idnumber' => $cattoset->idnumber,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function get_page_url_for_dynamic_submission(): moodle_url {
|
||||
$params = [];
|
||||
$cmid = isset($this->_ajaxformdata['cmid']) ? (int)$this->_ajaxformdata['cmid'] : 0;
|
||||
$courseid = isset($this->_ajaxformdata['courseid']) ? (int)$this->_ajaxformdata['courseid'] : 0;
|
||||
if ($cmid !== 0) {
|
||||
$params['cmid'] = $cmid;
|
||||
}
|
||||
|
||||
if ($courseid !== 0) {
|
||||
$params['courseid'] = $courseid;
|
||||
}
|
||||
return new moodle_url('/question/bank/managecategories/category.php', $params);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
|
||||
namespace qbank_managecategories;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once($CFG->libdir . "/questionlib.php");
|
||||
|
||||
use context;
|
||||
use core_question\category_manager;
|
||||
use core_question\local\bank\question_version_status;
|
||||
@@ -34,7 +38,6 @@ use html_writer;
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class helper {
|
||||
|
||||
/**
|
||||
* Name of this plugin.
|
||||
*/
|
||||
@@ -167,7 +170,11 @@ class helper {
|
||||
foreach ($categories[$id]->childids as $childid) {
|
||||
if ($childid != $nochildrenof) {
|
||||
$newcategories = $newcategories + self::flatten_category_tree(
|
||||
$categories, $childid, $depth + 1, $nochildrenof);
|
||||
$categories,
|
||||
$childid,
|
||||
$depth + 1,
|
||||
$nochildrenof,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,8 +205,10 @@ class helper {
|
||||
// categories from other courses, but not their parents.
|
||||
$toplevelcategoryids = [];
|
||||
foreach (array_keys($categories) as $id) {
|
||||
if (!empty($categories[$id]->parent) &&
|
||||
array_key_exists($categories[$id]->parent, $categories)) {
|
||||
if (
|
||||
!empty($categories[$id]->parent) &&
|
||||
array_key_exists($categories[$id]->parent, $categories)
|
||||
) {
|
||||
$categories[$categories[$id]->parent]->childids[] = $id;
|
||||
} else {
|
||||
$toplevelcategoryids[] = $id;
|
||||
@@ -210,7 +219,11 @@ class helper {
|
||||
$newcategories = [];
|
||||
foreach ($toplevelcategoryids as $id) {
|
||||
$newcategories = $newcategories + self::flatten_category_tree(
|
||||
$categories, $id, 0, $nochildrenof);
|
||||
$categories,
|
||||
$id,
|
||||
0,
|
||||
$nochildrenof,
|
||||
);
|
||||
}
|
||||
|
||||
return $newcategories;
|
||||
@@ -229,19 +242,36 @@ class helper {
|
||||
* default in the dropdown.
|
||||
* @param int $nochildrenof
|
||||
* @param bool $return to return the string of the select menu or echo that from the method
|
||||
* @return ?string The HTML, or null if the $return is false.
|
||||
* @throws \coding_exception|\dml_exception
|
||||
*/
|
||||
public static function question_category_select_menu(array $contexts, bool $top = false, int $currentcat = 0,
|
||||
string $selected = "", int $nochildrenof = -1, bool $return = false) {
|
||||
$categoriesarray = self::question_category_options($contexts, $top, $currentcat,
|
||||
false, $nochildrenof, false);
|
||||
public static function question_category_select_menu(
|
||||
array $contexts,
|
||||
bool $top = false,
|
||||
int $currentcat = 0,
|
||||
string $selected = "",
|
||||
int $nochildrenof = -1,
|
||||
bool $return = false,
|
||||
): ?string {
|
||||
$categoriesarray = self::question_category_options(
|
||||
$contexts,
|
||||
$top,
|
||||
$currentcat,
|
||||
false,
|
||||
$nochildrenof,
|
||||
false,
|
||||
);
|
||||
$choose = '';
|
||||
$options = [];
|
||||
foreach ($categoriesarray as $group => $opts) {
|
||||
$options[] = [$group => $opts];
|
||||
}
|
||||
$outputhtml = html_writer::label(get_string('questioncategory', 'core_question'),
|
||||
'id_movetocategory', false, ['class' => 'accesshide']);
|
||||
$outputhtml = html_writer::label(
|
||||
get_string('questioncategory', 'core_question'),
|
||||
'id_movetocategory',
|
||||
false,
|
||||
['class' => 'accesshide'],
|
||||
);
|
||||
$attrs = [
|
||||
'id' => 'id_movetocategory',
|
||||
'class' => 'custom-select',
|
||||
@@ -255,6 +285,7 @@ class helper {
|
||||
return $outputhtml;
|
||||
} else {
|
||||
echo $outputhtml;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,18 +293,22 @@ class helper {
|
||||
* Get all the category objects, including a count of the number of questions in that category,
|
||||
* for all the categories in the lists $contexts.
|
||||
*
|
||||
* @param context $contexts
|
||||
* @param string $contexts
|
||||
* @param string $sortorder used as the ORDER BY clause in the select statement.
|
||||
* @param bool $top Whether to return the top categories or not.
|
||||
* @param int $showallversions 1 to show all versions not only the latest.
|
||||
* @return array of category objects.
|
||||
* @throws \dml_exception
|
||||
*/
|
||||
public static function get_categories_for_contexts($contexts, string $sortorder = 'parent, sortorder, name ASC',
|
||||
bool $top = false, int $showallversions = 0): array {
|
||||
public static function get_categories_for_contexts(
|
||||
string $contexts,
|
||||
string $sortorder = 'parent, sortorder, name ASC',
|
||||
bool $top = false,
|
||||
int $showallversions = 0,
|
||||
): array {
|
||||
global $DB;
|
||||
$topwhere = $top ? '' : 'AND c.parent <> 0';
|
||||
$statuscondition = "AND (qv.status = '". question_version_status::QUESTION_STATUS_READY . "' " .
|
||||
$statuscondition = "AND (qv.status = '" . question_version_status::QUESTION_STATUS_READY . "' " .
|
||||
" OR qv.status = '" . question_version_status::QUESTION_STATUS_DRAFT . "' )";
|
||||
|
||||
$sql = "SELECT c.*,
|
||||
@@ -311,9 +346,14 @@ class helper {
|
||||
* @return array
|
||||
* @throws \coding_exception|\dml_exception
|
||||
*/
|
||||
public static function question_category_options(array $contexts, bool $top = false, int $currentcat = 0,
|
||||
bool $popupform = false, int $nochildrenof = -1,
|
||||
bool $escapecontextnames = true): array {
|
||||
public static function question_category_options(
|
||||
array $contexts,
|
||||
bool $top = false,
|
||||
int $currentcat = 0,
|
||||
bool $popupform = false,
|
||||
int $nochildrenof = -1,
|
||||
bool $escapecontextnames = true,
|
||||
): array {
|
||||
global $CFG;
|
||||
$pcontexts = [];
|
||||
foreach ($contexts as $context) {
|
||||
@@ -339,9 +379,12 @@ class helper {
|
||||
if ($category->contextid == $contextid) {
|
||||
$cid = $category->id;
|
||||
if ($currentcat != $cid || $currentcat == 0) {
|
||||
$a = new \stdClass;
|
||||
$a->name = format_string($category->indentedname, true,
|
||||
['context' => $context]);
|
||||
$a = new \stdClass();
|
||||
$a->name = format_string(
|
||||
$category->indentedname,
|
||||
true,
|
||||
['context' => $context]
|
||||
);
|
||||
if ($category->idnumber !== null && $category->idnumber !== '') {
|
||||
$a->idnumber = s($category->idnumber);
|
||||
}
|
||||
@@ -413,4 +456,14 @@ class helper {
|
||||
|
||||
return $categories;
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine id and context id for a question category
|
||||
*
|
||||
* @param \stdClass $category a category to extract its id and context id
|
||||
* @return string the combined string
|
||||
*/
|
||||
public static function combine_id_context(\stdClass $category): string {
|
||||
return $category->id . ',' . $category->contextid;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace qbank_managecategories\output;
|
||||
|
||||
use context;
|
||||
use qbank_managecategories\question_categories;
|
||||
use renderable;
|
||||
use renderer_base;
|
||||
use templatable;
|
||||
|
||||
/**
|
||||
* Output component for the Manage category page.
|
||||
*
|
||||
* This will return the template context for a page containing a tree of categories for each context in the provided
|
||||
* question_categories object, with editing controls.
|
||||
*
|
||||
* @package qbank_managecategories
|
||||
* @copyright 2024 onwards Catalyst IT EU {@link https://catalyst-eu.net}
|
||||
* @author Mark Johnson <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class categories implements renderable, templatable {
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param question_categories $categories Question categories for display.
|
||||
*/
|
||||
public function __construct(
|
||||
/** @var question_categories $categories Question categories for display. */
|
||||
protected question_categories $categories,
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function export_for_template(renderer_base $output): array {
|
||||
$categories = [];
|
||||
foreach ($this->categories->editlists as $contextid => $list) {
|
||||
// Get list elements.
|
||||
$context = context::instance_by_id($contextid);
|
||||
$itemstab = [];
|
||||
if (count($list->items)) {
|
||||
foreach ($list->items as $item) {
|
||||
$category = new category($item, $context);
|
||||
$itemstab['items'][] = $category->export_for_template($output);
|
||||
}
|
||||
}
|
||||
if (isset($itemstab['items'])) {
|
||||
$ctxlvl = "contextlevel" . $list->context->contextlevel;
|
||||
$contextname = $list->context->get_context_name();
|
||||
$heading = get_string('questioncatsfor', 'question', $contextname);
|
||||
|
||||
// Get categories context.
|
||||
$categories[] = [
|
||||
'ctxlvl' => $ctxlvl,
|
||||
'contextid' => $list->context->id,
|
||||
'contextname' => $contextname,
|
||||
'heading' => $heading,
|
||||
'items' => $itemstab['items'],
|
||||
'categoryid' => $list->categoryid,
|
||||
];
|
||||
}
|
||||
}
|
||||
$data = [
|
||||
'categoriesrendered' => $categories,
|
||||
'contextid' => $this->categories->contextid,
|
||||
'cmid' => $this->categories->cmid,
|
||||
'courseid' => $this->categories->courseid,
|
||||
'showdescriptions' => get_user_preferences('qbank_managecategories_showdescriptions'),
|
||||
];
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace qbank_managecategories\output;
|
||||
|
||||
use context;
|
||||
use renderable;
|
||||
use renderer_base;
|
||||
use templatable;
|
||||
use qbank_managecategories\question_categories;
|
||||
|
||||
/**
|
||||
* Output component for category page header.
|
||||
*
|
||||
* @package qbank_managecategories
|
||||
* @copyright 2024 onwards Catalyst IT EU {@link https://catalyst-eu.net}
|
||||
* @author Mark Johnson <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class categories_header implements renderable, templatable {
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param question_categories $categories Question categories for display.
|
||||
*/
|
||||
public function __construct(
|
||||
/** @var question_categories $categories Question categories for display. */
|
||||
protected question_categories $categories,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Output template data for the heading of the category management page.
|
||||
*
|
||||
* @param renderer_base $output
|
||||
* @return array
|
||||
* @throws \coding_exception
|
||||
*/
|
||||
public function export_for_template(renderer_base $output): array {
|
||||
$helpstringhead = $output->heading_with_help(
|
||||
get_string('editcategories', 'question'),
|
||||
'editcategories',
|
||||
'question',
|
||||
);
|
||||
$hascapability = has_capability(
|
||||
'moodle/question:managecategory',
|
||||
context::instance_by_id($this->categories->contextid),
|
||||
);
|
||||
|
||||
$data = [
|
||||
'helpstringhead' => $helpstringhead,
|
||||
'showdescriptions' => [
|
||||
'id' => 'showdescriptions-toggle',
|
||||
'checked' => get_user_preferences('qbank_managecategories_showdescriptions'),
|
||||
'label' => get_string('showcategorydescription', 'qbank_managecategories'),
|
||||
],
|
||||
'hascapability' => $hascapability,
|
||||
'contextid' => $this->categories->contextid,
|
||||
'cmid' => $this->categories->cmid,
|
||||
'courseid' => $this->categories->courseid,
|
||||
];
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace qbank_managecategories\output;
|
||||
|
||||
use action_menu;
|
||||
use action_menu_link;
|
||||
use context;
|
||||
use core\plugininfo\qbank;
|
||||
use core_question\category_manager;
|
||||
use moodle_url;
|
||||
use pix_icon;
|
||||
use qbank_managecategories\helper;
|
||||
use renderable;
|
||||
use renderer_base;
|
||||
use stdClass;
|
||||
use templatable;
|
||||
|
||||
/**
|
||||
* Output component for a single category
|
||||
*
|
||||
* @package qbank_managecategories
|
||||
* @copyright 2024 onwards Catalyst IT EU {@link https://catalyst-eu.net}
|
||||
* @author Mark Johnson <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class category implements renderable, templatable {
|
||||
/**
|
||||
* @var bool Can this category be reordered?
|
||||
*/
|
||||
protected bool $canreorder;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param stdClass $category The record of category we are rendering
|
||||
* @param context $context The context the category belongs to.
|
||||
* @param int $cmid The cmid of the course module context the category belongs to (optional).
|
||||
* @param int $courseid The course ID of the course context the category belongs to (optional).
|
||||
*/
|
||||
public function __construct(
|
||||
/** @var stdClass $category The record of category we are rendering */
|
||||
protected stdClass $category,
|
||||
/** @var context $context The context the category belongs to. */
|
||||
protected context $context,
|
||||
/** @var int $cmid The cmid of the course module context the category belongs to. */
|
||||
protected int $cmid = 0,
|
||||
/** @var int $courseid The course ID of the course context the category belongs to. */
|
||||
protected int $courseid = 0,
|
||||
) {
|
||||
$manager = new category_manager();
|
||||
$this->canreorder = !$manager->is_only_child_of_top_category_in_context($this->category->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the canreorder flag.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function get_canreorder(): bool {
|
||||
return $this->canreorder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the template data for a category, and call recursively for child categories.
|
||||
*
|
||||
* @param renderer_base $output
|
||||
* @return array
|
||||
*/
|
||||
public function export_for_template(renderer_base $output): array {
|
||||
global $PAGE;
|
||||
$canmanagecategory = has_capability('moodle/question:managecategory', $this->context);
|
||||
$params = $PAGE->url->params();
|
||||
$cmid = $params['cmid'] ?? $this->cmid;
|
||||
$courseid = $params['courseid'] ?? $this->courseid;
|
||||
|
||||
// Each section adds html to be displayed as part of this list item.
|
||||
$questionbankurl = new moodle_url('/question/edit.php', $params);
|
||||
$questionbankurl->param('cat', helper::combine_id_context($this->category));
|
||||
$categoryname = format_string($this->category->name, true, ['context' => $this->context]);
|
||||
$idnumber = null;
|
||||
if ($this->category->idnumber !== null && $this->category->idnumber !== '') {
|
||||
$idnumber = $this->category->idnumber;
|
||||
}
|
||||
$categorydesc = format_text(
|
||||
$this->category->info,
|
||||
$this->category->infoformat,
|
||||
['context' => $this->context, 'noclean' => true],
|
||||
);
|
||||
$menu = new action_menu();
|
||||
$menu->attributes['draggable'] = 'false';
|
||||
$menu->set_kebab_trigger();
|
||||
$menu->prioritise = true;
|
||||
|
||||
// Don't allow movement if only subcat.
|
||||
if ($canmanagecategory) {
|
||||
// This item display a modal for moving a category.
|
||||
// Move category modal.
|
||||
$menu->add(new action_menu_link(
|
||||
new \moodle_url('#'),
|
||||
new pix_icon(
|
||||
't/move',
|
||||
get_string('move'),
|
||||
'moodle',
|
||||
[
|
||||
'class' => 'iconsmall',
|
||||
]
|
||||
),
|
||||
get_string('move'),
|
||||
false,
|
||||
[
|
||||
'class' => 'show-when-movable', // Don't allow moving when this is the only category in the context.
|
||||
'data-categoryid' => $this->category->id,
|
||||
'data-actiontype' => 'move',
|
||||
'data-contextid' => (int) $this->category->contextid,
|
||||
'data-categoryname' => $categoryname,
|
||||
'title' => get_string('movecategory', 'qbank_managecategories', $categoryname),
|
||||
]
|
||||
));
|
||||
|
||||
$thiscontext = (int) $this->category->contextid;
|
||||
$editurl = new moodle_url('#');
|
||||
$menu->add(new action_menu_link(
|
||||
$editurl,
|
||||
new pix_icon('t/edit', 'edit'),
|
||||
get_string('editsettings'),
|
||||
false,
|
||||
[
|
||||
'data-action' => 'addeditcategory',
|
||||
'data-actiontype' => 'edit',
|
||||
'data-contextid' => $thiscontext,
|
||||
'data-categoryid' => $this->category->id,
|
||||
'data-cmid' => $cmid,
|
||||
'data-courseid' => $courseid,
|
||||
'data-questioncount' => $this->category->questioncount,
|
||||
]
|
||||
));
|
||||
// Sets up delete link.
|
||||
$deleteurl = new moodle_url(
|
||||
'/question/bank/managecategories/category.php',
|
||||
['delete' => $this->category->id, 'sesskey' => sesskey()]
|
||||
);
|
||||
if ($courseid !== 0) {
|
||||
$deleteurl->param('courseid', $courseid);
|
||||
} else {
|
||||
$deleteurl->param('cmid', $cmid);
|
||||
}
|
||||
$menu->add(new action_menu_link(
|
||||
$deleteurl,
|
||||
new pix_icon('t/delete', 'delete'),
|
||||
get_string('delete'),
|
||||
false,
|
||||
[
|
||||
'class' => 'text-danger show-when-movable', // Don't allow deletion when this is the only category in context.
|
||||
'data-confirmation' => 'modal',
|
||||
'data-confirmation-type' => 'delete',
|
||||
'data-confirmation-title-str' => json_encode(['delete', 'core']),
|
||||
'data-confirmation-yes-button-str' => json_encode(['delete', 'core']),
|
||||
'data-confirmation-content-str' => json_encode([
|
||||
'confirmdelete',
|
||||
'qbank_managecategories',
|
||||
$this->category->name,
|
||||
]),
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
// Sets up export to XML link.
|
||||
if (qbank::is_plugin_enabled('qbank_exportquestions')) {
|
||||
$exporturl = new moodle_url(
|
||||
'/question/bank/exportquestions/export.php',
|
||||
['cat' => helper::combine_id_context($this->category)]
|
||||
);
|
||||
if ($courseid !== 0) {
|
||||
$exporturl->param('courseid', $courseid);
|
||||
} else {
|
||||
$exporturl->param('cmid', $cmid);
|
||||
}
|
||||
|
||||
$menu->add(new action_menu_link(
|
||||
$exporturl,
|
||||
new pix_icon('t/download', 'download'),
|
||||
get_string('exportasxml', 'question'),
|
||||
false,
|
||||
));
|
||||
}
|
||||
|
||||
$children = [];
|
||||
if (!empty($this->category->children)) {
|
||||
foreach ($this->category->children as $child) {
|
||||
$childcategory = new category($child, $this->context);
|
||||
$children[] = $childcategory->export_for_template($output);
|
||||
}
|
||||
}
|
||||
$itemdata = [
|
||||
'categoryid' => $this->category->id,
|
||||
'contextid' => $this->category->contextid,
|
||||
'questionbankurl' => $questionbankurl->out(false),
|
||||
'categoryname' => $categoryname,
|
||||
'idnumber' => $idnumber,
|
||||
'questioncount' => $this->category->questioncount,
|
||||
'categorydesc' => $categorydesc,
|
||||
'editactionmenu' => $menu->export_for_template($output),
|
||||
'draghandle' => $canmanagecategory && $this->canreorder,
|
||||
'haschildren' => !empty($children),
|
||||
'children' => $children,
|
||||
'parent' => $this->category->parent,
|
||||
'sortorder' => $this->category->sortorder,
|
||||
'newchildtooltip' => get_string('newchild', 'qbank_managecategories', $categoryname),
|
||||
];
|
||||
return $itemdata;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -16,6 +16,9 @@
|
||||
|
||||
namespace qbank_managecategories\privacy;
|
||||
|
||||
use core_privacy\local\metadata\collection;
|
||||
use core_privacy\local\request\writer;
|
||||
|
||||
/**
|
||||
* Privacy Subsystem for qbank_managecategories implementing null_provider.
|
||||
*
|
||||
@@ -25,14 +28,35 @@ namespace qbank_managecategories\privacy;
|
||||
* @author Guillermo Gomez Arias <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class provider implements \core_privacy\local\metadata\null_provider {
|
||||
class provider implements
|
||||
\core_privacy\local\metadata\provider,
|
||||
\core_privacy\local\request\user_preference_provider {
|
||||
/**
|
||||
* Get the language string identifier with the component's language
|
||||
* file to explain why this plugin stores no data.
|
||||
* Returns meta data about this system.
|
||||
*
|
||||
* @return string
|
||||
* @param collection $collection The initialised collection to add items to.
|
||||
* @return collection A listing of user data stored through this system.
|
||||
*/
|
||||
public static function get_reason(): string {
|
||||
return 'privacy:metadata';
|
||||
public static function get_metadata(collection $collection): collection {
|
||||
$collection->add_user_preference('qbank_managecategories_showdescriptions', 'privacy:preference:showdescriptions');
|
||||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all user preferences for the plugin.
|
||||
*
|
||||
* @param int $userid The userid of the user whose data is to be exported.
|
||||
*/
|
||||
public static function export_user_preferences(int $userid) {
|
||||
$showdescription = get_user_preferences('qbank_managecategories_showdescriptions', null, $userid);
|
||||
if ($showdescription !== null) {
|
||||
$displaydescription = $showdescription ? 'displaydescription' : 'descriptionnotdisplayed';
|
||||
writer::export_user_preference(
|
||||
'qbank_managecategories',
|
||||
'showdescr',
|
||||
$showdescription,
|
||||
get_string($displaydescription, 'qbank_managecategories')
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,13 +16,6 @@
|
||||
|
||||
namespace qbank_managecategories;
|
||||
|
||||
/**
|
||||
* QUESTION_PAGE_LENGTH - Number of categories to display on page.
|
||||
*/
|
||||
if (!defined('QUESTION_PAGE_LENGTH')) {
|
||||
define('QUESTION_PAGE_LENGTH', 25);
|
||||
}
|
||||
|
||||
use context;
|
||||
use moodle_url;
|
||||
|
||||
|
||||
@@ -30,7 +30,10 @@ use moodle_list;
|
||||
* @package qbank_managecategories
|
||||
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @deprecated MDL-72397 Since Moodle 4.5
|
||||
* @todo Final removal in Moodle 6.0 MDL-80804
|
||||
*/
|
||||
#[\core\attribute\deprecated(null, since: '4.5', mdl: 'MDL-72397', reason: 'Output is now generated by templates.')]
|
||||
class question_category_list extends moodle_list {
|
||||
|
||||
/**
|
||||
@@ -80,18 +83,25 @@ class question_category_list extends moodle_list {
|
||||
* @param string $pageparamname name of url param that is used for passing page no
|
||||
* @param integer $itemsperpage no of top level items.
|
||||
* @param \context $context
|
||||
* @deprecated MDL-72397 Since Moodle 4.5
|
||||
* @todo Final removal in Moodle 6.0 MDL-80804
|
||||
*/
|
||||
public function __construct($type='ul', $attributes='', $editable = false, $pageurl=null,
|
||||
$page = 0, $pageparamname = 'page',
|
||||
$itemsperpage = DEFAULT_QUESTIONS_PER_PAGE, $context = null) {
|
||||
\core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]);
|
||||
parent::__construct('ul', '', $editable, $pageurl, $page, 'cpage', $itemsperpage);
|
||||
$this->context = $context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the array of records of list items.
|
||||
*
|
||||
* @deprecated MDL-72397 Since Moodle 4.5
|
||||
* @todo Final removal in Moodle 6.0 MDL-80804
|
||||
*/
|
||||
public function get_records(): void {
|
||||
\core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]);
|
||||
$this->records = helper::get_categories_for_contexts($this->context->id, $this->sortby);
|
||||
}
|
||||
|
||||
@@ -101,8 +111,11 @@ class question_category_list extends moodle_list {
|
||||
*
|
||||
* @param \list_item $item The item which its top level parent is going to be returned.
|
||||
* @return int
|
||||
* @deprecated MDL-72397 Since Moodle 4.5
|
||||
* @todo Final removal in Moodle 6.0 MDL-80804
|
||||
*/
|
||||
public function get_top_level_parent_id($item): int {
|
||||
\core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]);
|
||||
// Put the item at the highest level it can go.
|
||||
$topcategory = question_get_top_category($item->item->contextid, true);
|
||||
return $topcategory->id;
|
||||
@@ -116,8 +129,11 @@ class question_category_list extends moodle_list {
|
||||
* @param integer $moveup id of item to move up
|
||||
* @param integer $movedown id of item to move down
|
||||
* @return void
|
||||
* @deprecated MDL-72397 Since Moodle 4.5
|
||||
* @todo Final removal in Moodle 6.0 MDL-80804
|
||||
*/
|
||||
public function process_actions($left, $right, $moveup, $movedown): void {
|
||||
\core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]);
|
||||
$category = new stdClass();
|
||||
if (!empty($left)) {
|
||||
// Moved Left (In to another category).
|
||||
|
||||
@@ -24,7 +24,10 @@ use moodle_url;
|
||||
* @package qbank_managecategories
|
||||
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @deprecated MDL-72397 Since Moodle 4.5
|
||||
* @todo Final removal in Moodle 6.0 MDL-80804
|
||||
*/
|
||||
#[\core\attribute\deprecated(null, since: '4.5', mdl: 'MDL-72397', reason: 'Output is now generated by templates.')]
|
||||
class question_category_list_item extends \list_item {
|
||||
|
||||
/**
|
||||
@@ -33,8 +36,10 @@ class question_category_list_item extends \list_item {
|
||||
* @param bool $first Is the first on the list.
|
||||
* @param bool $last Is the last on the list.
|
||||
* @param \list_item $lastitem Last item.
|
||||
* @deprecated MDL-72397 Since Moodle 4.5
|
||||
*/
|
||||
public function set_icon_html($first, $last, $lastitem): void {
|
||||
\core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]);
|
||||
global $CFG;
|
||||
$category = $this->item;
|
||||
$url = new moodle_url('/question/bank/managecategories/category.php',
|
||||
@@ -74,8 +79,10 @@ class question_category_list_item extends \list_item {
|
||||
* @param array $extraargs
|
||||
* @return string Item html.
|
||||
* @throws \moodle_exception
|
||||
* @deprecated MDL-72397 Since Moodle 4.5
|
||||
*/
|
||||
public function item_html($extraargs = []): string {
|
||||
\core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]);
|
||||
global $PAGE, $OUTPUT;
|
||||
$str = $extraargs['str'];
|
||||
$category = $this->item;
|
||||
|
||||
@@ -34,6 +34,8 @@ use stdClass;
|
||||
* @package qbank_managecategories
|
||||
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @deprecated Since Moodle 4.5 MDL-72397. Use \core_question\category_manager or \qbank_managecategories\question_categories.
|
||||
* @todo Final removal in Moodle 6.0 MDL-80804
|
||||
*/
|
||||
class question_category_object {
|
||||
|
||||
@@ -77,9 +79,17 @@ class question_category_object {
|
||||
* @param int|null $defaultcategory id of the current category. null if none.
|
||||
* @param int $todelete id of the category to delete. 0 if none.
|
||||
* @param context[] $addcontexts contexts where the current user can add questions.
|
||||
* @deprecated Since Moodle 4.5 MDL-72397. Use \qbank_managecategories\question_categories instead.
|
||||
* @todo Final removal in Moodle 6.0 MDL-80804
|
||||
*/
|
||||
#[\core\attribute\deprecated(
|
||||
'\qbank_managecategories\question_categories',
|
||||
since: 4.5,
|
||||
reason: 'API properly divided between qbank_managecategories and core_question',
|
||||
mdl: 'MDL-72397',
|
||||
)]
|
||||
public function __construct($page, $pageurl, $contexts, $currentcat, $defaultcategory, $todelete, $addcontexts) {
|
||||
|
||||
\core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]);
|
||||
$this->tab = str_repeat(' ', $this->tabsize);
|
||||
|
||||
$this->str = new stdClass();
|
||||
@@ -118,8 +128,17 @@ class question_category_object {
|
||||
* @param int|null $defaultcategory id of the current category. null if none.
|
||||
* @param int $todelete id of the category to delete. 0 if none.
|
||||
* @param context[] $addcontexts contexts where the current user can add questions.
|
||||
* @deprecated Since Moodle 4.5 MDL-72397. Use \qbank_managecategories\question_categories instead.
|
||||
* @todo Final removal in Moodle 6.0 MDL-80804
|
||||
*/
|
||||
#[\core\attribute\deprecated(
|
||||
'\qbank_managecategories\question_categories',
|
||||
since: 4.5,
|
||||
reason: 'API properly divided between qbank_managecategories and core_question',
|
||||
mdl: 'MDL-72397',
|
||||
)]
|
||||
public function initialize($page, $contexts, $currentcat, $defaultcategory, $todelete, $addcontexts): void {
|
||||
\core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]);
|
||||
$lastlist = null;
|
||||
foreach ($contexts as $context) {
|
||||
$this->editlists[$context->id] =
|
||||
@@ -146,16 +165,35 @@ class question_category_object {
|
||||
/**
|
||||
* Displays the user interface.
|
||||
*
|
||||
* @deprecated Since Moodle 4.5 MDL-72397. No longer used.
|
||||
* @todo Final removal in Moodle 6.0 MDL-80804
|
||||
*/
|
||||
#[\core\attribute\deprecated(
|
||||
'\core_question\category_manager::get_real_question_ids_in_category',
|
||||
since: 4.5,
|
||||
reason: 'No longer used',
|
||||
mdl: 'MDL-72397',
|
||||
)]
|
||||
public function display_user_interface(): void {
|
||||
\core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]);
|
||||
// Interface for editing existing categories.
|
||||
$this->output_edit_lists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Outputs a table to allow entry of a new category
|
||||
*
|
||||
* @deprecated Since Moodle 4.5 MDL-72397. No longer used.
|
||||
* @todo Final removal in Moodle 6.0 MDL-80804
|
||||
*/
|
||||
#[\core\attribute\deprecated(
|
||||
'\core_question\category_manager::get_real_question_ids_in_category',
|
||||
since: 4.5,
|
||||
reason: 'No longer used',
|
||||
mdl: 'MDL-72397',
|
||||
)]
|
||||
public function output_new_table(): void {
|
||||
\core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]);
|
||||
$this->catform->display();
|
||||
}
|
||||
|
||||
@@ -164,8 +202,17 @@ class question_category_object {
|
||||
*
|
||||
* $this->initialize() must have already been called
|
||||
*
|
||||
* @deprecated Since Moodle 4.5 MDL-72397. No longer used.
|
||||
* @todo Final removal in Moodle 6.0 MDL-80804
|
||||
*/
|
||||
#[\core\attribute\deprecated(
|
||||
'\core_question\category_manager::get_real_question_ids_in_category',
|
||||
since: 4.5,
|
||||
reason: 'No longer used',
|
||||
mdl: 'MDL-72397',
|
||||
)]
|
||||
public function output_edit_lists(): void {
|
||||
\core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]);
|
||||
global $OUTPUT;
|
||||
|
||||
echo $OUTPUT->heading_with_help(get_string('questioncategories', 'question'), 'editcategories', 'question');
|
||||
@@ -189,8 +236,17 @@ class question_category_object {
|
||||
*
|
||||
* @param array $categories contains category objects in a tree representation
|
||||
* @return array courseids flat array in form categoryid=>courseid
|
||||
* @deprecated Since Moodle 4.5 MDL-72397. No longer used.
|
||||
* @todo Final removal in Moodle 6.0 MDL-80804
|
||||
*/
|
||||
#[\core\attribute\deprecated(
|
||||
'\core_question\category_manager::get_real_question_ids_in_category',
|
||||
since: 4.5,
|
||||
reason: 'No longer used',
|
||||
mdl: 'MDL-72397',
|
||||
)]
|
||||
public function get_course_ids(array $categories): array {
|
||||
\core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]);
|
||||
$courseids = [];
|
||||
foreach ($categories as $key => $cat) {
|
||||
$courseids[$key] = $cat->course;
|
||||
@@ -205,8 +261,17 @@ class question_category_object {
|
||||
* Edit a category, or add a new one if the id is zero.
|
||||
*
|
||||
* @param int $categoryid Category id.
|
||||
* @deprecated Since Moodle 4.5 MDL-72397. No longer used.
|
||||
* @todo Final removal in Moodle 6.0 MDL-80804
|
||||
*/
|
||||
#[\core\attribute\deprecated(
|
||||
'\core_question\category_manager::get_real_question_ids_in_category',
|
||||
since: 4.5,
|
||||
reason: 'No longer used',
|
||||
mdl: 'MDL-72397',
|
||||
)]
|
||||
public function edit_single_category(int $categoryid): void {
|
||||
\core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]);
|
||||
// Interface for adding a new category.
|
||||
global $DB;
|
||||
|
||||
@@ -235,9 +300,17 @@ class question_category_object {
|
||||
*
|
||||
* @param array $parentstrings a list of parentstrings
|
||||
* @param object $category Category object
|
||||
* @deprecated Since Moodle 4.5 MDL-72397. No longer used.
|
||||
* @todo Final removal in Moodle 6.0 MDL-80804
|
||||
*/
|
||||
#[\core\attribute\deprecated(
|
||||
'\core_question\category_manager::get_real_question_ids_in_category',
|
||||
since: 4.5,
|
||||
reason: 'No longer used',
|
||||
mdl: 'MDL-72397',
|
||||
)]
|
||||
public function set_viable_parents(array &$parentstrings, object $category): void {
|
||||
|
||||
\core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]);
|
||||
unset($parentstrings[$category->id]);
|
||||
if (isset($category->children)) {
|
||||
foreach ($category->children as $child) {
|
||||
@@ -252,8 +325,17 @@ class question_category_object {
|
||||
* @param int|null $parent - if given, restrict records to those with this parent id.
|
||||
* @param string $sort - [[sortfield [,sortfield]] {ASC|DESC}].
|
||||
* @return array categories.
|
||||
* @deprecated Since Moodle 4.5 MDL-72397. No longer used.
|
||||
* @todo Final removal in Moodle 6.0 MDL-80804
|
||||
*/
|
||||
#[\core\attribute\deprecated(
|
||||
'\core_question\category_manager::get_real_question_ids_in_category',
|
||||
since: 4.5,
|
||||
reason: 'No longer used',
|
||||
mdl: 'MDL-72397',
|
||||
)]
|
||||
public function get_question_categories(?int $parent = null, string $sort = "sortorder ASC"): array {
|
||||
\core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]);
|
||||
global $COURSE, $DB;
|
||||
if (is_null($parent)) {
|
||||
$categories = $DB->get_records('question_categories', ['course' => $COURSE->id], $sort);
|
||||
@@ -268,8 +350,17 @@ class question_category_object {
|
||||
* Deletes an existing question category.
|
||||
*
|
||||
* @param int $categoryid id of category to delete.
|
||||
* @deprecated Since Moodle 4.5 MDL-72397. Use \core_question\category_manager instead.
|
||||
* @todo Final removal in Moodle 6.0 MDL-80804
|
||||
*/
|
||||
#[\core\attribute\deprecated(
|
||||
'\core_question\category_manager::delete_category',
|
||||
since: 4.5,
|
||||
reason: 'API properly divided between qbank_managecategories and core_question',
|
||||
mdl: 'MDL-72397',
|
||||
)]
|
||||
public function delete_category(int $categoryid): void {
|
||||
\core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]);
|
||||
global $CFG, $DB;
|
||||
helper::question_can_delete_cat($categoryid);
|
||||
if (!$category = $DB->get_record("question_categories", ["id" => $categoryid])) { // Security.
|
||||
@@ -293,8 +384,17 @@ class question_category_object {
|
||||
*
|
||||
* @param int $oldcat id of the old category.
|
||||
* @param int $newcat id of the new category.
|
||||
* @deprecated Since Moodle 4.5 MDL-72397. Use \core_question\category_manager instead.
|
||||
* @todo Final removal in Moodle 6.0 MDL-80804
|
||||
*/
|
||||
#[\core\attribute\deprecated(
|
||||
'\core_question\category_manager::move_questions_and_delete_category',
|
||||
since: 4.5,
|
||||
reason: 'API properly divided between qbank_managecategories and core_question',
|
||||
mdl: 'MDL-72397',
|
||||
)]
|
||||
public function move_questions_and_delete_category(int $oldcat, int $newcat): void {
|
||||
\core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]);
|
||||
helper::question_can_delete_cat($oldcat);
|
||||
$this->move_questions($oldcat, $newcat);
|
||||
$this->delete_category($oldcat);
|
||||
@@ -310,6 +410,7 @@ class question_category_object {
|
||||
* @deprecated No longer used by internal code and not recommended since Moodle 4.2 MDL-77299.
|
||||
*/
|
||||
public function display_move_form($questionsincategory, $category): void {
|
||||
\core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]);
|
||||
debugging(
|
||||
'display_move_form() is deprecated and no longer used by internal code.',
|
||||
DEBUG_DEVELOPER
|
||||
@@ -328,8 +429,17 @@ class question_category_object {
|
||||
* @param int $oldcat id of the old category.
|
||||
* @param int $newcat id of the new category.
|
||||
* @throws \dml_exception
|
||||
* @deprecated Since Moodle 4.5 MDL-72397. Use \core_question\category_manager instead.
|
||||
* @todo Final removal in Moodle 6.0 MDL-80804
|
||||
*/
|
||||
#[\core\attribute\deprecated(
|
||||
'\core_question\category_manager::move_questions',
|
||||
since: 4.5,
|
||||
reason: 'API properly divided between qbank_managecategories and core_question',
|
||||
mdl: 'MDL-72397',
|
||||
)]
|
||||
public function move_questions(int $oldcat, int $newcat): void {
|
||||
\core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]);
|
||||
$questionids = $this->get_real_question_ids_in_category($oldcat);
|
||||
question_move_questions_to_category($questionids, $newcat);
|
||||
}
|
||||
@@ -348,9 +458,18 @@ class question_category_object {
|
||||
* @param int|string $newinfoformat description format. One of the FORMAT_ constants.
|
||||
* @param null $idnumber the idnumber. '' is converted to null.
|
||||
* @return bool|int New category id if successful, else false.
|
||||
* @deprecated Since Moodle 4.5 MDL-72397. Use \core_question\category_manager instead.
|
||||
* @todo Final removal in Moodle 6.0 MDL-80804
|
||||
*/
|
||||
#[\core\attribute\deprecated(
|
||||
'\core_question\category_manager::add_category',
|
||||
since: 4.5,
|
||||
reason: 'API properly divided between qbank_managecategories and core_question',
|
||||
mdl: 'MDL-72397',
|
||||
)]
|
||||
public function add_category($newparent, $newcategory, $newinfo, $return = false, $newinfoformat = FORMAT_HTML,
|
||||
$idnumber = null): int {
|
||||
\core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]);
|
||||
global $DB;
|
||||
if (empty($newcategory)) {
|
||||
throw new moodle_exception('categorynamecantbeblank', 'question');
|
||||
@@ -414,9 +533,18 @@ class question_category_object {
|
||||
* @param int|string $newinfoformat description format. One of the FORMAT_ constants.
|
||||
* @param int $idnumber the idnumber. '' is converted to null.
|
||||
* @param bool $redirect if true, will redirect once the DB is updated (default).
|
||||
* @deprecated Since Moodle 4.5 MDL-72397. Use \core_question\category_manager instead.
|
||||
* @todo Final removal in Moodle 6.0 MDL-80804
|
||||
*/
|
||||
#[\core\attribute\deprecated(
|
||||
'\core_question\category_manager::update_category',
|
||||
since: 4.5,
|
||||
reason: 'API properly divided between qbank_managecategories and core_question',
|
||||
mdl: 'MDL-72397',
|
||||
)]
|
||||
public function update_category($updateid, $newparent, $newname, $newinfo, $newinfoformat = FORMAT_HTML,
|
||||
$idnumber = null, $redirect = true): void {
|
||||
\core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]);
|
||||
global $CFG, $DB;
|
||||
if (empty($newname)) {
|
||||
throw new moodle_exception('categorynamecantbeblank', 'question');
|
||||
@@ -501,8 +629,17 @@ class question_category_object {
|
||||
*
|
||||
* @param int $categoryid id of the category.
|
||||
* @return int[] array of question ids.
|
||||
* @deprecated Since Moodle 4.5 MDL-72397. Use \core_question\category_manager instead.
|
||||
* @todo Final removal in Moodle 6.0 MDL-80804
|
||||
*/
|
||||
#[\core\attribute\deprecated(
|
||||
'\core_question\category_manager::get_real_question_ids_in_category',
|
||||
since: 4.5,
|
||||
reason: 'API properly divided between qbank_managecategories and core_question',
|
||||
mdl: 'MDL-72397',
|
||||
)]
|
||||
public function get_real_question_ids_in_category(int $categoryid): array {
|
||||
\core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]);
|
||||
global $DB;
|
||||
|
||||
$sql = "SELECT q.id
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* qbank_managecategories external functions and service definitions.
|
||||
* @package qbank_managecategories
|
||||
* @category webservice
|
||||
* @copyright 2021 Catalyst IT Australia Pty Ltd
|
||||
* @author 2021, Ghaly Marc-Alexandre <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$functions = [
|
||||
'qbank_managecategories_move_category' => [
|
||||
'classname' => 'qbank_managecategories\external\move_category',
|
||||
'description' => 'Move a question category',
|
||||
'type' => 'write',
|
||||
'capabilities' => 'moodle/question:managecategory',
|
||||
'ajax' => true,
|
||||
],
|
||||
];
|
||||
@@ -23,5 +23,18 @@
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
$string['after'] = 'After {$a}';
|
||||
$string['before'] = 'Before {$a}';
|
||||
$string['categoryincategory'] = 'Cannot move category in same category.';
|
||||
$string['categorymoved'] = 'The category has been moved successfully.';
|
||||
$string['confirmdelete'] = 'Delete category "{$a}"?. Any child categories will be moved to this category\'s parent.';
|
||||
$string['descriptionnotdisplayed'] = 'Categories description not displayed';
|
||||
$string['displaydescription'] = 'Categories description displayed';
|
||||
$string['idnumberexists'] = 'ID number already in use, please change it to move or update category';
|
||||
$string['lastcategoryinthiscontext'] = 'This category is the only present under context,
|
||||
please add another category if this one is to be moved';
|
||||
$string['movecategory'] = 'Move {$a}';
|
||||
$string['newchild'] = 'As new child of {$a}';
|
||||
$string['pluginname'] = 'Manage categories';
|
||||
$string['privacy:metadata'] = 'The Manage categories question bank plugin does not store any personal data.';
|
||||
$string['privacy:preference:showdescriptions'] = 'A flag to indicate whether or not category descriptions are shown.';
|
||||
$string['showcategorydescription'] = 'Show descriptions';
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Callbacks for qbank_manageacategories
|
||||
*
|
||||
* @package qbank_managecategories
|
||||
* @copyright 2024 Catalyst IT Europe Ltd
|
||||
* @author Mark Johnson <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
/**
|
||||
* Allow update of user preferences via AJAX.
|
||||
*
|
||||
* @return array[]
|
||||
*/
|
||||
function qbank_managecategories_user_preferences(): array {
|
||||
return [
|
||||
'qbank_managecategories_showdescriptions' => [
|
||||
'type' => PARAM_BOOL,
|
||||
'null' => NULL_NOT_ALLOWED,
|
||||
'default' => false,
|
||||
'permissioncallback' => [core_user::class, 'is_current_user'],
|
||||
],
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/* Card-style border on category lists */
|
||||
.qbank_managecategories-categorylist {
|
||||
padding: 1rem;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
/* Category item layout */
|
||||
.qbank_managecategories-handlecontainer {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.qbank_managecategories-item .container {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
/* Separators between each item become drop indicators */
|
||||
.qbank_managecategories-item {
|
||||
width: 100%;
|
||||
list-style: none;
|
||||
border-top: 1px solid #dee2e6;
|
||||
}
|
||||
.qbank_managecategories-item:first-child {
|
||||
border-top-color: transparent;
|
||||
}
|
||||
.qbank_managecategories-item:last-child {
|
||||
border-bottom: 1px solid transparent;
|
||||
}
|
||||
.qbank_managecategories-item .qbank_managecategories-item:first-child {
|
||||
border-top-color: #dee2e6;
|
||||
}
|
||||
.qbank_managecategories-item.qbank_managecategories-droptarget-before,
|
||||
.qbank_managecategories-item .qbank_managecategories-item.qbank_managecategories-droptarget-before,
|
||||
.qbank_managecategories-categorylist.qbank_managecategories-droptarget-before > .qbank_managecategories-item:first-child {
|
||||
border-top-color: black;
|
||||
}
|
||||
.qbank_managecategories-item.qbank_managecategories-droptarget,
|
||||
.qbank_managecategories-categorylist.qbank_managecategories-droptarget > .qbank_managecategories-item:last-child {
|
||||
border-bottom-color: black;
|
||||
}
|
||||
.qbank_managecategories-details p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.qbank_managecategories-childlistcontainer:empty {
|
||||
display: none;
|
||||
}
|
||||
/* Fade effect on drag handle */
|
||||
.qbank_managecategories-draghandle {
|
||||
display: none;
|
||||
opacity: 0.45;
|
||||
}
|
||||
.qbank_managecategories-item.draghandle .qbank_managecategories-draghandle {
|
||||
display: inline;
|
||||
}
|
||||
/* Drag indicator */
|
||||
.qbank_managecategories-item.draghandle > .container {
|
||||
border: 2px solid transparent;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
.qbank_managecategories-item.draghandle > .container:hover {
|
||||
cursor: move;
|
||||
border-color: #0f6cbf;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
/* No move/delete buttons on non-draggable items */
|
||||
.dropdown-item.show-when-movable {
|
||||
display: none;
|
||||
}
|
||||
.qbank_managecategories-item.draghandle .dropdown-item.show-when-movable {
|
||||
display: block;
|
||||
}
|
||||
/* Don't show card border on nested lists. */
|
||||
.qbank_managecategories-categorylist .qbank_managecategories-categorylist {
|
||||
padding: 0 0 .5rem 0;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
/* Don't show hover indication on parent when a child is hovered */
|
||||
.editing .qbank_managecategories-item .container:hover:has(.container:hover) {
|
||||
border-color: transparent;
|
||||
}
|
||||
/* New child drop target */
|
||||
.qbank_managecategories-newchild {
|
||||
display: none;
|
||||
border: 1px solid #dee2e6;
|
||||
background-color: #fff;
|
||||
padding: 0.25rem 1rem;
|
||||
color: #dee2e6;
|
||||
font-weight: bold;
|
||||
}
|
||||
.qbank_managecategories-newchild.qbank_managecategories-droptarget,
|
||||
/* Show the drop target if it's directly inside a highlighted category, but not if its inside a child of that category */
|
||||
.qbank_managecategories-droptarget-before > .container > .row > .qbank_managecategories-contentcontainer > :not(.qbank_managecategories-childlistcontainer) .qbank_managecategories-newchild {
|
||||
display: unset;
|
||||
}
|
||||
.qbank_managecategories-newchild.qbank_managecategories-droptarget {
|
||||
border-color: black;
|
||||
color: black;
|
||||
}
|
||||
/* Hidden descriptions */
|
||||
.qbank_managecategories-description {
|
||||
display: none;
|
||||
}
|
||||
.showdescriptions .qbank_managecategories-description {
|
||||
display: unset;
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template qbank_managecategories/addcategory
|
||||
|
||||
Add category button
|
||||
|
||||
Context variables required for this template:
|
||||
* contextid - Context id for js init.
|
||||
* cmid - Course module id, if in a plugin.
|
||||
* courseid - The current course ID.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"contextid": "18",
|
||||
"cmid": "1234",
|
||||
"courseid": "1234"
|
||||
}
|
||||
}}
|
||||
<div id="qbank_managecategories-addcategory" class="pl-3">
|
||||
<button type="submit" data-action="addeditcategory" data-actiontype="add"
|
||||
data-contextid="{{contextid}}"
|
||||
data-cmid="{{cmid}}" data-courseid="{{courseid}}" class="btn btn-primary">
|
||||
{{# str}} addcategory, core_question {{/ str}}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{#js}}
|
||||
require(['qbank_managecategories/addcategory'], (addcategory) => {
|
||||
addcategory.init('#qbank_managecategories-addcategory');
|
||||
});
|
||||
{{/js}}
|
||||
@@ -0,0 +1,79 @@
|
||||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template qbank_managecategories/categories
|
||||
|
||||
Template for displaying category view.
|
||||
|
||||
Context variables required for this template:
|
||||
* contextid - Context id for js init.
|
||||
* categoriesrendered - Array representing categories data rendered.
|
||||
* items - Categories data.
|
||||
* heading - Category's heading for each context (ie: Course or Quiz).
|
||||
* ctxlvl - Context level for categories section - context.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"contextid": "18",
|
||||
"categoriesrendered": [{
|
||||
"items": [{
|
||||
"categoryid": "1",
|
||||
"questionbankurl": "question/edit.php?cmid=123",
|
||||
"categoryname": "Default for Miscellaneous",
|
||||
"idnumber": "1",
|
||||
"questioncount": " (1)",
|
||||
"categorydesc": "The default category for questions shared in context Miscellaneous",
|
||||
"editactionmenu": "<div class='action-menu moodle-actionmenu'>...</div>",
|
||||
"handle": true,
|
||||
"iconleft": "<i class=\"icon fa fa-arrow-left fa-fw \" title=\"Move to top level\" role=\"img\" aria-label=\"Move to top level\"></i>",
|
||||
"iconright": "<i class=\"icon fa fa-arrow-right fa-fw \" title=\"Make child of 'category'\" role=\"img\" aria-label=\"Make child of 'category'\"></i>",
|
||||
"children": ""
|
||||
}],
|
||||
"heading": "Question categories for 'Quiz: qz'",
|
||||
"ctxlvl": "contextlevel70"
|
||||
}]
|
||||
}
|
||||
}}
|
||||
<div id="categoryroot"
|
||||
class="editing col-11 col-md-12 pl-0{{#showdescriptions}} showdescriptions{{/showdescriptions}}"
|
||||
data-contextid="{{{contextid}}}">
|
||||
{{#categoriesrendered}}
|
||||
<h3 class="context-heading">{{heading}}</h3>
|
||||
<div class="box py-3 boxwidthwide boxaligncenter generalbox questioncategories {{ctxlvl}}">
|
||||
<ul class="qbank_managecategories-categorylist"
|
||||
data-contextid="{{contextid}}" data-contextname="{{contextname}}" data-categoryid="{{categoryid}}">
|
||||
{{#items}}
|
||||
{{> qbank_managecategories/category }}
|
||||
{{/items}}
|
||||
</ul>
|
||||
</div>
|
||||
{{/categoriesrendered}}
|
||||
</div>
|
||||
|
||||
|
||||
{{#js}}
|
||||
require(['qbank_managecategories/categoryroot'], (categoryroot) => {
|
||||
categoryroot.init('#categoryroot');
|
||||
});
|
||||
require(['qbank_managecategories/categorylist'], (categorylist) => {
|
||||
// Initialise top-level lists.
|
||||
const categoryLists = document.querySelectorAll('.qbank_managecategories-categorylist[data-contextid]');
|
||||
categoryLists.forEach(element => {
|
||||
categorylist.init(`.qbank_managecategories-categorylist[data-categoryid="${element.dataset.categoryid}"]`);
|
||||
});
|
||||
});
|
||||
{{/js}}
|
||||
@@ -0,0 +1,51 @@
|
||||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template qbank_managecategories/categories_header
|
||||
|
||||
Template for displaying category view.
|
||||
|
||||
Context variables required for this template:
|
||||
* helpstringhead - String header accompanied with it help button.
|
||||
* hascapability - Boolean, true if user has capability to add categories.
|
||||
* checkbox - Checkbox displaying descriptions.
|
||||
* contextid - Context id for js init.
|
||||
* cmid - Course module id, if in a plugin.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"helpstringhead": "<h2> Edit categories <a href='someurl'>...</a></h2>",
|
||||
"hascapability": true,
|
||||
"checkbox": "<form>...</form>",
|
||||
"contextid": "18",
|
||||
"cmid": "1234",
|
||||
"courseid": "1234"
|
||||
}
|
||||
}}
|
||||
<div class="row">
|
||||
<div class="pl-3">
|
||||
{{{helpstringhead}}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
{{#hascapability}}
|
||||
{{>qbank_managecategories/addcategory}}
|
||||
{{/hascapability}}
|
||||
{{#showdescriptions}}
|
||||
{{>qbank_managecategories/showdescriptions}}
|
||||
{{/showdescriptions}}
|
||||
</div>
|
||||
@@ -0,0 +1,84 @@
|
||||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template qbank_managecategories/category
|
||||
|
||||
This template renders the list item for each category.
|
||||
|
||||
Context variables required for this template:
|
||||
* categoryid - The id of the category this item represents.
|
||||
* questionbankurl - Url for corresponding question bank.
|
||||
* categoryname - Category name.
|
||||
* idnumber - Id number string (optional).
|
||||
* questioncount - Number of question in question bank.
|
||||
* categorydesc - Category description.
|
||||
* editactionmenu - Edit action menu link.
|
||||
* draghandle - Boolean if handle should be displayed (if category is movable).
|
||||
* children - Children category item to curent category (same variables as those above).
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"categoryid": "1",
|
||||
"questionbankurl": "question/edit.php?cmid=123",
|
||||
"categoryname": "Default for Miscellaneous",
|
||||
"idnumber": "1",
|
||||
"questioncount": " 1",
|
||||
"categorydesc": "The default category for questions shared in context Miscellaneous",
|
||||
"editactionmenu": "<div class='action-menu moodle-actionmenu'>...</div>",
|
||||
"draghandle": true,
|
||||
"children": ""
|
||||
}
|
||||
}}
|
||||
<li class="qbank_managecategories-item{{#draghandle}} draghandle{{/draghandle}}"
|
||||
id="category-{{{categoryid}}}"
|
||||
data-categoryid="{{{categoryid}}}"
|
||||
data-contextid="{{{contextid}}}"
|
||||
data-categoryname="{{categoryname}}"
|
||||
data-parent="{{parent}}"
|
||||
data-sortorder="{{sortorder}}">
|
||||
<div class="container{{#haschildren}} pb-0{{/haschildren}}">
|
||||
<div class="row m-0">
|
||||
<div class="p-2 mr-2 qbank_managecategories-handlecontainer">
|
||||
<span class="qbank_managecategories-draghandle">
|
||||
<i class="fa fa-grip-vertical"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-11 p-0 qbank_managecategories-contentcontainer">
|
||||
<div class="row m-0">
|
||||
<div class="col-11 p-0 pt-1 qbank_managecategories-details">
|
||||
{{>qbank_managecategories/category_details}}
|
||||
</div>
|
||||
<div class="col-1 qbank_managecategories-actioncontainer">
|
||||
{{#editactionmenu}}
|
||||
{{>core/action_menu}}
|
||||
{{/editactionmenu}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row m-0 mt-1 qbank_managecategories-childlistcontainer">
|
||||
{{#haschildren}}
|
||||
{{> qbank_managecategories/childlist }}
|
||||
{{/haschildren}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{{#js}}
|
||||
require(['qbank_managecategories/category'], function(component) {
|
||||
component.init('#category-{{{categoryid}}}');
|
||||
});
|
||||
{{/js}}
|
||||
@@ -0,0 +1,64 @@
|
||||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template qbank_managecategories/category_details
|
||||
|
||||
Partial template for rendering category details. Name, question count, tags, description, etc.
|
||||
|
||||
Context variables required for this template:
|
||||
* categoryid - The id of the category this item represents.
|
||||
* questionbankurl - Url for corresponding question bank.
|
||||
* categoryname - Category name.
|
||||
* idnumber - Id number string (optional).
|
||||
* questioncount - Number of question in question bank.
|
||||
* categorydesc - Category description.
|
||||
* editactionmenu - Edit action menu link.
|
||||
* draghandle - Boolean if handle should be displayed (if category is movable).
|
||||
* children - Children category item to curent category (same variables as those above).
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"categoryid": "1",
|
||||
"questionbankurl": "question/edit.php?cmid=123",
|
||||
"categoryname": "Default for Miscellaneous",
|
||||
"idnumber": "1",
|
||||
"questioncount": " 1",
|
||||
"categorydesc": "The default category for questions shared in context Miscellaneous",
|
||||
"children": "",
|
||||
"newchildtooltip": "New child of Default for Miscellaneous"
|
||||
}
|
||||
}}
|
||||
<strong>
|
||||
<a title="{{#str}}editquestions, question{{/str}}" href="{{{questionbankurl}}}" role="button">
|
||||
{{{categoryname}}}
|
||||
{{#idnumber}}
|
||||
<span class="badge badge-primary">
|
||||
<span class="accesshide">
|
||||
{{#str}}idnumber, question{{/str}}
|
||||
</span>
|
||||
{{idnumber}}
|
||||
</span>
|
||||
{{/idnumber}}
|
||||
({{questioncount}})
|
||||
</a>
|
||||
</strong>
|
||||
{{^children}}
|
||||
{{> qbank_managecategories/newchild }}
|
||||
{{/children}}
|
||||
<div class="qbank_managecategories-description">
|
||||
{{{categorydesc}}}
|
||||
</div>
|
||||
@@ -0,0 +1,52 @@
|
||||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template qbank_managecategories/childlist
|
||||
|
||||
This template a sub-list of children within a category.
|
||||
|
||||
Context variables required for this template:
|
||||
* categoryid - The parent category of the child categories.
|
||||
* children - Children category item to curent category (same variables as qbank_managecategories/category).
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"categoryid": "1",
|
||||
"children": [
|
||||
{
|
||||
"categoryid": "2",
|
||||
"questionbankurl": "question/edit.php?cmid=123",
|
||||
"categoryname": "Default for Miscellaneous",
|
||||
"idnumber": "1",
|
||||
"questioncount": " 1",
|
||||
"categorydesc": "The default category for questions shared in context Miscellaneous",
|
||||
"editactionmenu": "<div class='action-menu moodle-actionmenu'>...</div>",
|
||||
"draghandle": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}}
|
||||
<ul class="qbank_managecategories-categorylist row mt-1 col-12" data-categoryid="{{categoryid}}">
|
||||
{{#children}}
|
||||
{{> qbank_managecategories/category }}
|
||||
{{/children}}
|
||||
</ul>
|
||||
{{#js}}
|
||||
require(['qbank_managecategories/categorylist'], function(component) {
|
||||
component.init('.qbank_managecategories-categorylist[data-categoryid="{{categoryid}}"]');
|
||||
});
|
||||
{{/js}}
|
||||
@@ -49,7 +49,7 @@
|
||||
</a>
|
||||
</b>
|
||||
{{#categorydesc}}
|
||||
<div class="text_to_html">
|
||||
<div>
|
||||
{{{categorydesc}}}
|
||||
</div>
|
||||
{{/categorydesc}}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template qbank_managecategories/move_category_list
|
||||
|
||||
Partial to render a list of categories that the current category can be moved next to.
|
||||
|
||||
This partial is included recursively to render nested lists. Take care to pass categories = null for bottom-level
|
||||
categories, to avoid infinite recursion.
|
||||
|
||||
Context variables required for this template:
|
||||
* categories - Array of category objects.
|
||||
* categoryname - The name of the category to move to.
|
||||
* categoryid - The ID of the category.
|
||||
* firstchild - Is this the first child of its parent? If so, a "before" option will be rendered as well as "after".
|
||||
* current - Is this the category being moved? If so, skip rendering it as a target.
|
||||
* categories - Array containing child categories of this category.
|
||||
If there are none, this must be set null or [] to avoid infinite recursion.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"categories": [
|
||||
{
|
||||
"categoryname": "Default category for course 1",
|
||||
"categoryid": 3,
|
||||
"firstchild": true,
|
||||
"current": false,
|
||||
"hascategories": false,
|
||||
"categories": []
|
||||
},
|
||||
{
|
||||
"categoryname": "Test category 1",
|
||||
"categoryid": 4,
|
||||
"firstchild": false,
|
||||
"current": false,
|
||||
"hascategories": false,
|
||||
"categories": []
|
||||
},
|
||||
{
|
||||
"categoryname": "Test category 2",
|
||||
"categoryid": 5,
|
||||
"firstchild": false,
|
||||
"current": true,
|
||||
"hascategories": false,
|
||||
"categories": []
|
||||
},
|
||||
{
|
||||
"categoryname": "Test category 3 x < 1 && y > 2 ",
|
||||
"categoryid": 6,
|
||||
"firstchild": false,
|
||||
"current": false,
|
||||
"hascategories": true,
|
||||
"categories": [
|
||||
{
|
||||
"categoryname": "Test category 4",
|
||||
"categoryid": 7,
|
||||
"firstchild": true,
|
||||
"current": false,
|
||||
"hascategories": false,
|
||||
"categories": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}}
|
||||
|
||||
<ul data-for="categorylist" data-contextid="{{contextid}}" role="group">
|
||||
{{#categories}}
|
||||
{{^lastchild}}
|
||||
<li class="list_item d-flex" role="treeitem">
|
||||
<a class="modal_category_item text-truncate" {{!
|
||||
}}href="#" {{!
|
||||
}}data-for="category" {{!
|
||||
}}data-movingcategoryid="{{movingcategoryid}}" {{!
|
||||
}}data-precedingsiblingid="{{precedingsiblingid}}" {{!
|
||||
}}data-parent="{{parent}}">
|
||||
{{#newchild}}
|
||||
{{#str}} newchild, qbank_managecategories, {{{categoryname}}} {{/str}}
|
||||
{{/newchild}}
|
||||
{{^newchild}}
|
||||
{{#str}} before, qbank_managecategories, {{{categoryname}}} {{/str}}
|
||||
{{/newchild}}
|
||||
</a>
|
||||
</li>
|
||||
<li class="list_item d-flex" role="treeitem">
|
||||
{{>qbank_managecategories/move_category_list}}
|
||||
</li>
|
||||
{{/lastchild}}
|
||||
{{#lastchild}}
|
||||
<li class="list_item d-flex" role="treeitem">
|
||||
<a class="modal_category_item text-truncate" {{!
|
||||
}}href="#" {{!
|
||||
}}data-for="category" {{!
|
||||
}}data-movingcategoryid="{{movingcategoryid}}" {{!
|
||||
}}data-precedingsiblingid="{{precedingsiblingid}}" {{!
|
||||
}}data-parent="{{parent}}">
|
||||
{{#str}} after, qbank_managecategories, {{{categoryname}}} {{/str}}
|
||||
</a>
|
||||
</li>
|
||||
{{/lastchild}}
|
||||
{{/categories}}
|
||||
</ul>
|
||||
@@ -0,0 +1,83 @@
|
||||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template qbank_managecategories/move_context_list
|
||||
|
||||
Modal to move a category.
|
||||
|
||||
Context variables required for this template:
|
||||
* contexts - Array of contexts containing question categories
|
||||
* contextid - The id of the context.
|
||||
* contextname - The name of the context to display as a heading.
|
||||
* categories - Array of category objects. See qbank_managecategories/move_category_list partial for details.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"contexts": [
|
||||
{
|
||||
"contexid": 1,
|
||||
"contextname": "Course 1",
|
||||
"categories": [
|
||||
{
|
||||
"categoryname": "Default category for course 1",
|
||||
"categoryid": 3,
|
||||
"firstchild": true,
|
||||
"current": false,
|
||||
"hascategories": false,
|
||||
"categories": []
|
||||
},
|
||||
{
|
||||
"categoryname": "Default category for course category 1",
|
||||
"categoryid": 4,
|
||||
"firstchild": false,
|
||||
"current": true,
|
||||
"hascategories": false,
|
||||
"categories": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"contexid": 3,
|
||||
"contextname": "Course 2",
|
||||
"categories": [
|
||||
{
|
||||
"categoryname": "Default category for course 2",
|
||||
"categoryid": 5,
|
||||
"firstchild": true,
|
||||
"current": false,
|
||||
"hascategories": false,
|
||||
"categories": []
|
||||
},
|
||||
{
|
||||
"categoryname": "Default category for course category 2",
|
||||
"categoryid": 6,
|
||||
"firstchild": false,
|
||||
"current": false,
|
||||
"hascategories": false,
|
||||
"categories": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}}
|
||||
<div role="tree">
|
||||
{{#contexts}}
|
||||
<h3>{{{contextname}}}</h3>
|
||||
{{>qbank_managecategories/move_category_list}}
|
||||
{{/contexts}}
|
||||
</div>
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template qbank_managecategories/newchild
|
||||
|
||||
Drop zone for creating a new child category.
|
||||
|
||||
Context variables required for this template:
|
||||
* categoryid - The id of the parent category.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"categoryid": "1",
|
||||
"newchildtooltip": "As new child of Example"
|
||||
}
|
||||
}}
|
||||
<span
|
||||
class="qbank_managecategories-newchild"
|
||||
title="{{newchildtooltip}}"
|
||||
data-parent="{{categoryid}}"
|
||||
data-toggle="tooltip"
|
||||
data-trigger="manual"
|
||||
data-placement="top"
|
||||
>
|
||||
+
|
||||
</span>
|
||||
|
||||
{{#js}}
|
||||
require(['qbank_managecategories/newchild'], function(component) {
|
||||
component.init('.qbank_managecategories-newchild[data-parent="{{categoryid}}"]');
|
||||
});
|
||||
{{/js}}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template qbank_managecategories/showdescriptions
|
||||
|
||||
Template for displaying the Show descriptions toggle.
|
||||
|
||||
Context variables required for this template:
|
||||
* helpstringhead - String header accompanied with it help button.
|
||||
* hascapability - Boolean, true if user has capability to add categories.
|
||||
* checkbox - Checkbox displaying descriptions.
|
||||
* contextid - Context id for js init.
|
||||
* cmid - Course module id, if in a plugin.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"id": "showdescriptions-toggle",
|
||||
"label": "Show descriptions",
|
||||
"checked": true
|
||||
}
|
||||
}}
|
||||
<div class="col-6 mt-2" id="qbank_managecategories-showdescriptions">
|
||||
{{>core/toggle}}
|
||||
</div>
|
||||
{{#js}}
|
||||
require(['qbank_managecategories/showdescriptions'], function(component) {
|
||||
component.init('#qbank_managecategories-showdescriptions');
|
||||
});
|
||||
{{/js}}
|
||||
@@ -1,43 +1,101 @@
|
||||
@core @core_question
|
||||
Feature: A teacher can move question categories in the question bank
|
||||
In order to organize my questions
|
||||
@qbank @qbank_managecategories @category_reorder @javascript
|
||||
Feature: A teacher can reorder question categories
|
||||
In order to change question category order
|
||||
As a teacher
|
||||
I create question categories and move them in the question bank
|
||||
I need to reorder them
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | T1 | Teacher1 | teacher1@example.com |
|
||||
| teacher1 | Teacher | 1 | teacher1@example.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
| fullname | shortname | format |
|
||||
| Course 1 | C1 | weeks |
|
||||
And the following "categories" exist:
|
||||
| name | category | idnumber |
|
||||
| Category 1 | 0 | CAT1 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
And the following "activities" exist:
|
||||
| activity | name | course | idnumber |
|
||||
| quiz | Test quiz | C1 | quiz1 |
|
||||
And the following "system role assigns" exist:
|
||||
| user | role | contextlevel |
|
||||
| teacher1 | editingteacher | System |
|
||||
And the following "question categories" exist:
|
||||
| contextlevel | reference | name | idnumber |
|
||||
| Course | C1 | Course category 1 | questioncat1 |
|
||||
| Course | C1 | Course category 2 | questioncat2 |
|
||||
| Course | C1 | Course category 3 | questioncat3 |
|
||||
| Category | CAT1 | Default for Category 1 | |
|
||||
| System | S1 | System category | |
|
||||
And I am on the "Course 1" "core_question > course question categories" page logged in as "teacher1"
|
||||
|
||||
Scenario: A question category can be moved to another context
|
||||
Given I am on the "Test quiz" "mod_quiz > question bank" page logged in as "teacher1"
|
||||
And I select "Categories" from the "Question bank tertiary navigation" singleselect
|
||||
And I follow "Add category"
|
||||
And I set the following fields to these values:
|
||||
| Name | Test category |
|
||||
| Parent category | Top for Test quiz |
|
||||
And I press "submitbutton"
|
||||
And I click on "Share in context for Course: Course 1" "link" in the "Test category" "list_item"
|
||||
Then I should see "Test category" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' questioncategories ') and contains(concat(' ', normalize-space(@class), ' '), ' contextlevel50 ')]" "xpath_element"
|
||||
Scenario: Teacher cannot move or delete single category under context
|
||||
When I open the action menu in "Default for Category 1" "list_item"
|
||||
Then I should not see "Delete"
|
||||
|
||||
Scenario: A question category can be moved to top level
|
||||
Given I am on the "Test quiz" "mod_quiz > question bank" page logged in as "teacher1"
|
||||
And I select "Categories" from the "Question bank tertiary navigation" singleselect
|
||||
And I follow "Add category"
|
||||
And I set the following fields to these values:
|
||||
| Name | Test category |
|
||||
| Parent category | Default for Test quiz |
|
||||
| Category info | Created as a test |
|
||||
And I press "submitbutton"
|
||||
And I click on "Move to top level" "link" in the "Test category" "list_item"
|
||||
Then I should see "Test category" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' questioncategories ') and contains(concat(' ', normalize-space(@class), ' '), ' contextlevel70 ')]" "xpath_element"
|
||||
And "//div[contains(concat(' ', normalize-space(@class), ' '), ' questioncategories ') and contains(concat(' ', normalize-space(@class), ' '), ' contextlevel70 ')]//li//ul" "xpath_element" should not exist
|
||||
Scenario: Teacher can see complete edit menu if multiples categories exist under context
|
||||
When I open the action menu in "Course category 1" "list_item"
|
||||
Then I should see "Edit settings"
|
||||
And I should see "Delete"
|
||||
And I should see "Export as Moodle XML"
|
||||
|
||||
Scenario: Teacher can move one category after another
|
||||
Given "Course category 1" "list_item" should appear before "Course category 2" "list_item"
|
||||
And "Course category 2" "list_item" should appear before "Course category 3" "list_item"
|
||||
When I open the action menu in "Course category 1" "list_item"
|
||||
And I choose "Move" in the open action menu
|
||||
And I click on "After Course category 3" "link" in the "Move Course category 1" "dialogue"
|
||||
Then "Course category 2" "list_item" should appear before "Course category 3" "list_item"
|
||||
And "Course category 3" "list_item" should appear before "Course category 1" "list_item"
|
||||
|
||||
Scenario: Teacher can move one category before another
|
||||
Given "Course category 1" "list_item" should appear before "Course category 2" "list_item"
|
||||
And "Course category 2" "list_item" should appear before "Course category 3" "list_item"
|
||||
And I open the action menu in "Course category 3" "list_item"
|
||||
And I choose "Move" in the open action menu
|
||||
And I click on "Before Course category 1" "link" in the "Move Course category 3" "dialogue"
|
||||
Given "Course category 3" "list_item" should appear before "Course category 1" "list_item"
|
||||
And "Course category 1" "list_item" should appear before "Course category 2" "list_item"
|
||||
|
||||
Scenario: Teacher can make a category a child of an existing category
|
||||
Given "Course category 1" "list_item" should appear before "Course category 2" "list_item"
|
||||
And "Course category 2" "list_item" should appear before "Course category 3" "list_item"
|
||||
And "Course category 3" "list_item" should not exist in the "Course category 1" "list_item"
|
||||
When I open the action menu in "Course category 3" "list_item"
|
||||
And I choose "Move" in the open action menu
|
||||
And I click on "As new child of Course category 1" "link" in the "Move Course category 3" "dialogue"
|
||||
And "Course category 3" "list_item" should appear before "Course category 2" "list_item"
|
||||
And "Course category 3" "list_item" should exist in the "Course category 1" "list_item"
|
||||
|
||||
Scenario: Teacher can move a category between contexts
|
||||
Given "Course: Course 1" "text" should appear before "Course category 1" "list_item"
|
||||
And "Category: Category 1" "text" should appear after "Course category 1" "list_item"
|
||||
And "Category: Category 1" "text" should appear before "Default for Category 1" "list_item"
|
||||
And "Course category 2" "list_item" should appear before "Course category 3" "list_item"
|
||||
When I open the action menu in "Course category 1" "list_item"
|
||||
And I choose "Move" in the open action menu
|
||||
And I click on "After Default for Category 1" "link" in the "Move Course category 1" "dialogue"
|
||||
Then "Course: Course 1" "text" should appear before "Course category 1" "list_item"
|
||||
And "Category: Category 1" "text" should appear before "Course category 1" "list_item"
|
||||
And "Default for Category 1" "list_item" should appear before "Course category 1" "list_item"
|
||||
|
||||
Scenario: Teacher can display and hide category descriptions
|
||||
When I click on "Show descriptions" "checkbox"
|
||||
Then I should see "The default category for questions shared in context 'Category 1'."
|
||||
And I click on "Show descriptions" "checkbox"
|
||||
And I should not see "The default category for questions shared in context 'Category 1'."
|
||||
|
||||
Scenario: Teacher cannot create a duplicate idnumber within a context by moving a category
|
||||
Given "Course category 1" "list_item" should appear before "System category" "list_item"
|
||||
And I open the action menu in "Course category 1" "list_item"
|
||||
And I choose "Move" in the open action menu
|
||||
And I click on "After System category" "link" in the "Move Course category 1" "dialogue"
|
||||
Then "Course category 1" "list_item" should appear after "System category" "list_item"
|
||||
And I open the action menu in "Course category 2" "list_item"
|
||||
And I choose "Edit settings" in the open action menu
|
||||
And I set the field "ID number" to "questioncat1"
|
||||
And I click on "Save changes" "button" in the "Edit category" "dialogue"
|
||||
And I open the action menu in "Course category 2" "list_item"
|
||||
And I choose "Move" in the open action menu
|
||||
And I click on "After Course category 1" "link" in the "Move Course category 2" "dialogue"
|
||||
And I should see "ID number already in use, please change it to move or update category"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@qbank @qbank_managecategories @javascript
|
||||
@qbank @qbank_managecategories @question_categories @javascript
|
||||
Feature: A teacher can put questions in categories in the question bank
|
||||
In order to organize my questions
|
||||
As a teacher
|
||||
@@ -6,44 +6,44 @@ Feature: A teacher can put questions in categories in the question bank
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | 1 | teacher1@example.com |
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | 1 | teacher1@example.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | format |
|
||||
| Course 1 | C1 | weeks |
|
||||
| Course 1 | C1 | weeks |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
| 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 | Default for C1 | Another subcat |
|
||||
| Course | C1 | top | Used category |
|
||||
| Course | C1 | top | Default & testing |
|
||||
| contextlevel | reference | questioncategory | name | sortorder | desciption |
|
||||
| Course | C1 | Top | top | 0 | |
|
||||
| Course | C1 | top | Default for C1 | 0 | Description for default for C1 |
|
||||
| Course | C1 | Default for C1 | Subcategory | 0 | Description for Subcategory |
|
||||
| Course | C1 | Default for C1 | Another subcat | 1 | Description for Another subcat |
|
||||
| Course | C1 | top | Used category | 1 | |
|
||||
| Course | C1 | top | Default & testing | 2 | |
|
||||
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
|
||||
|
||||
Scenario: A new question category can be created
|
||||
When I am on the "Course 1" "core_question > course question categories" page
|
||||
And I follow "Add category"
|
||||
And I press "Add category"
|
||||
And I set the following fields to these values:
|
||||
| Name | 'Test' & 'display' |
|
||||
| Parent category | Default & testing |
|
||||
| Category info | Created for testing category, HTML entity & its encoding |
|
||||
| ID number | newcatidnumber |
|
||||
And I press "submitbutton"
|
||||
And I click on "Add category" "button" in the "Add category" "dialogue"
|
||||
Then I should see "Default & testing"
|
||||
And I should see "ID number"
|
||||
And I should see "newcatidnumber"
|
||||
And I should see "(0)"
|
||||
And I click on "Show descriptions" "checkbox"
|
||||
And I should see "Created for testing category, HTML entity & its encoding" in the "'Test' & 'display'" "list_item"
|
||||
And I follow "Add category"
|
||||
And I press "Add category"
|
||||
And the "Parent category" select box should contain "'Test' & 'display' [newcatidnumber]"
|
||||
|
||||
Scenario: A question category can be edited
|
||||
@@ -52,29 +52,35 @@ Feature: A teacher can put questions in categories in the question bank
|
||||
And the following "questions" exist:
|
||||
| questioncategory | qtype | name | questiontext |
|
||||
| Subcategory | essay | Test question for renaming category | Write about whatever you want |
|
||||
And I click on "Edit this category" "link" in the "Subcategory" "list_item"
|
||||
And I open the action menu in "Subcategory" "list_item"
|
||||
And I choose "Edit settings" in the open action menu
|
||||
And the field "parent" matches value " Default for C1"
|
||||
And I set the following fields to these values:
|
||||
| Name | New name |
|
||||
| Category info | I was edited |
|
||||
And I press "Save changes"
|
||||
Then I should see "New name"
|
||||
And I should see "I was edited" in the "New name" "list_item"
|
||||
And I click on "Show descriptions" "checkbox"
|
||||
And I should see "I was edited"
|
||||
|
||||
Scenario: An empty question category can be deleted
|
||||
When I am on the "Course 1" "core_question > course question categories" page
|
||||
And I click on "Delete" "link" in the "Subcategory" "list_item"
|
||||
And I open the action menu in "Subcategory" "list_item"
|
||||
And I choose "Delete" in the open action menu
|
||||
And I click on "Delete" "button" in the "Delete" "dialogue"
|
||||
Then I should not see "Subcategory"
|
||||
|
||||
Scenario: An non-empty question category can be deleted if you move the contents elsewhere
|
||||
When I am on the "Course 1" "core_question > course question categories" page
|
||||
And I click on "Delete" "link" in the "Used category" "list_item"
|
||||
And I open the action menu in "Used category" "list_item"
|
||||
And I choose "Delete" in the open action menu
|
||||
And I click on "Delete" "button" in the "Delete" "dialogue"
|
||||
And I should see "The category 'Used category' contains 1 questions"
|
||||
And I select "Default for C1" from the "Category" singleselect
|
||||
And I press "Save in category"
|
||||
Then I should not see "Used category"
|
||||
And I follow "Add category"
|
||||
And I should see "Default for C1 (1)"
|
||||
And I should see "Default for C1"
|
||||
And I should see "(1)"
|
||||
|
||||
@_file_upload
|
||||
Scenario: Multi answer questions with their child questions can be moved to another category when the current category is deleted
|
||||
@@ -84,12 +90,14 @@ Feature: A teacher can put questions in categories in the question bank
|
||||
And I press "id_submitbutton"
|
||||
And I press "Continue"
|
||||
And I am on the "Course 1" "core_question > course question categories" page
|
||||
And I click on "Delete" "link" in the "Default for Test images in backup" "list_item"
|
||||
And I open the action menu in "Default for Test images in backup" "list_item"
|
||||
And I choose "Delete" in the open action menu
|
||||
And I click on "Delete" "button" in the ".modal-footer" "css_element"
|
||||
And I should see "The category 'Default for Test images in backup' contains 1 questions"
|
||||
And I select "Used category" from the "Category" singleselect
|
||||
And I press "Save in category"
|
||||
Then I should not see "Default for Test images in backup"
|
||||
And I follow "Add category"
|
||||
And I press "Add category"
|
||||
And I should see "Used category (2)"
|
||||
|
||||
Scenario: Filter questions by category and subcategories
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@qbank @qbank_managecategories
|
||||
@qbank @qbank_managecategories @question_categories_idnumber @javascript
|
||||
Feature: A teacher can put questions with idnumbers in categories with idnumbers in the question bank
|
||||
In order to organize my questions
|
||||
As a teacher
|
||||
@@ -20,34 +20,36 @@ Feature: A teacher can put questions with idnumbers in categories with idnumbers
|
||||
Scenario: A new question category can only be created with a unique idnumber for a context
|
||||
# Note need to create the top category each time.
|
||||
When the following "question categories" exist:
|
||||
| contextlevel | reference | questioncategory | name | idnumber |
|
||||
| Course | C1 | Top | top | |
|
||||
| Course | C1 | top | Used category | c1used |
|
||||
| contextlevel | reference | questioncategory | name | idnumber |
|
||||
| Course | C1 | Top | top | |
|
||||
| Course | C1 | top | Used category | c1used |
|
||||
And I am on the "Course 1" "core_question > course question categories" page
|
||||
And I follow "Add category"
|
||||
And I press "Add category"
|
||||
And I set the following fields to these values:
|
||||
| Name | Sub used category |
|
||||
| Parent category | Used category |
|
||||
| Name | New cat |
|
||||
| Parent category | Top for Course 1 |
|
||||
| Category info | Created as a test |
|
||||
| ID number | c1used |
|
||||
And I press "submitbutton"
|
||||
And I click on "Add category" "button" in the "Add category" "dialogue"
|
||||
# Standard warning.
|
||||
Then I should see "This ID number is already in use"
|
||||
# Correction to a unique idnumber for the context.
|
||||
And I set the field "ID number" to "c1unused"
|
||||
And I press "submitbutton"
|
||||
Then I should see "Sub used category"
|
||||
And I click on "Add category" "button" in the "Add category" "dialogue"
|
||||
Then I should see "New cat"
|
||||
And I should see "ID number"
|
||||
And I should see "c1unused"
|
||||
And I should see "(0)"
|
||||
And I should see "Created as a test" in the "Sub used category" "list_item"
|
||||
And I click on "Show descriptions" "checkbox"
|
||||
And I should see "Created as a test" in the "New cat" "list_item"
|
||||
|
||||
Scenario: A question category can be edited and saved without changing the idnumber
|
||||
When the following "question categories" exist:
|
||||
| contextlevel | reference | questioncategory | name | idnumber |
|
||||
| Course | C1 | Top | top | |
|
||||
| Course | C1 | top | Used category | c1used |
|
||||
| contextlevel | reference | questioncategory | name | idnumber |
|
||||
| Course | C1 | Top | top | |
|
||||
| Course | C1 | top | Used category | c1used |
|
||||
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 press "Save changes"
|
||||
Then I open the action menu in "Used category" "list_item"
|
||||
And I choose "Edit settings" in the open action menu
|
||||
And I click on "Save changes" "button" in the "Edit category" "dialogue"
|
||||
Then I should not see "This ID number is already in use"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@qbank @qbank_managecategories @javascript
|
||||
@qbank @qbank_managecategories @view_manage_categories_plugin @javascript
|
||||
Feature: Use the qbank plugin manager page for managecategories
|
||||
In order to check the plugin behaviour with enable and disable
|
||||
|
||||
@@ -7,8 +7,8 @@ Feature: Use the qbank plugin manager page for managecategories
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
And the following "activities" exist:
|
||||
| activity | name | course | idnumber |
|
||||
| quiz | Test quiz | C1 | quiz1 |
|
||||
| activity | name | course | idnumber |
|
||||
| quiz | Test quiz | C1 | quiz1 |
|
||||
And the following "question categories" exist:
|
||||
| contextlevel | reference | name |
|
||||
| Course | C1 | Test questions |
|
||||
@@ -26,7 +26,7 @@ Feature: Use the qbank plugin manager page for managecategories
|
||||
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" "mod_quiz > question bank" page
|
||||
And I should see "Categories" in the "//div[contains(@class, 'urlselect')]//option[contains(text(), 'Categories')]" "xpath_element"
|
||||
And I should see "Categories" in the "Question bank tertiary navigation" "select"
|
||||
|
||||
Scenario: Enable/disable the tab New category when trying to add a random question to a quiz
|
||||
Given I log in as "admin"
|
||||
|
||||
@@ -0,0 +1,537 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace qbank_managecategories\external;
|
||||
|
||||
use context;
|
||||
use context_course;
|
||||
use moodle_url;
|
||||
use qbank_managecategories\question_categories;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once(__DIR__ . '/../manage_category_test_base.php');
|
||||
|
||||
/**
|
||||
* Unit tests for move_category
|
||||
*
|
||||
* @package qbank_managecategories
|
||||
* @copyright 2024 onwards Catalyst IT EU {@link https://catalyst-eu.net}
|
||||
* @author Mark Johnson <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @covers \qbank_managecategories\external\move_category
|
||||
*/
|
||||
final class move_category_test extends \qbank_managecategories\manage_category_test_base {
|
||||
|
||||
/**
|
||||
* Return order of categories for a given context.
|
||||
*
|
||||
* @param context $context The context to get the category order for.
|
||||
* @return array Nested array, keyed by category IDs.
|
||||
*/
|
||||
private function get_current_order(context $context): array {
|
||||
$categories = new question_categories(new moodle_url('/'), [$context]);
|
||||
return $this->reduce_tree($categories->editlists[$context->id]->items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduce the ordered tree of categories to a multi-dimensional array of IDs for easier comparison.
|
||||
*
|
||||
* @param array $tree Tree of categories from question_categories.
|
||||
* @return array
|
||||
*/
|
||||
private function reduce_tree(array $tree): array {
|
||||
$result = [];
|
||||
foreach ($tree as $category) {
|
||||
$result[$category->id] = [];
|
||||
if (isset($category->children) && !empty((array)$category->children)) {
|
||||
$result[$category->id] = $this->reduce_tree($category->children);
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a category below another category within the same parent.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test_move_category_down(): void {
|
||||
$this->setAdminUser();
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Create context for question categories.
|
||||
$course = $this->create_course();
|
||||
$context = context_course::instance($course->id);
|
||||
$this->create_course_category();
|
||||
|
||||
// Question categories.
|
||||
$qcat1 = $this->create_question_category_for_a_course($course);
|
||||
$qcat2 = $this->create_question_category_for_a_course($course);
|
||||
$qcat3 = $this->create_question_category_for_a_course($course);
|
||||
|
||||
// Check current order.
|
||||
$currentorder = $this->get_current_order($context);
|
||||
$expectedorder = [
|
||||
$qcat1->id => [],
|
||||
$qcat2->id => [],
|
||||
$qcat3->id => [],
|
||||
];
|
||||
$this->assertEquals($expectedorder, $currentorder);
|
||||
|
||||
// Move category 1 after category 2.
|
||||
$stateupdates = move_category::execute($context->id, $qcat1->id, $qcat1->parent, $qcat2->id);
|
||||
|
||||
$neworder = $this->get_current_order($context);
|
||||
$newexpectedorder = [
|
||||
$qcat2->id => [],
|
||||
$qcat1->id => [],
|
||||
$qcat3->id => [],
|
||||
];
|
||||
$this->assertEquals($newexpectedorder, $neworder);
|
||||
|
||||
// We should have an update to the sortorder of the moved category and following categories.
|
||||
$expectedstateupdates = [
|
||||
(object)[
|
||||
'name' => 'categories',
|
||||
'action' => 'put',
|
||||
'fields' => (object)[
|
||||
'id' => $qcat1->id,
|
||||
'sortorder' => 3,
|
||||
],
|
||||
],
|
||||
(object)[
|
||||
'name' => 'categories',
|
||||
'action' => 'put',
|
||||
'fields' => (object)[
|
||||
'id' => $qcat3->id,
|
||||
'sortorder' => 4,
|
||||
],
|
||||
],
|
||||
];
|
||||
$this->assertEquals($stateupdates, $expectedstateupdates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a category to the top of its parent.
|
||||
*
|
||||
* @return void
|
||||
* @throws \moodle_exception
|
||||
*/
|
||||
public function test_move_category_to_top(): void {
|
||||
$this->setAdminUser();
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Create context for question categories.
|
||||
$course = $this->create_course();
|
||||
$context = context_course::instance($course->id);
|
||||
$this->create_course_category();
|
||||
|
||||
// Question categories.
|
||||
$qcat1 = $this->create_question_category_for_a_course($course);
|
||||
$qcat2 = $this->create_question_category_for_a_course($course);
|
||||
$qcat3 = $this->create_question_category_for_a_course($course);
|
||||
|
||||
// Check current order.
|
||||
$currentorder = $this->get_current_order($context);
|
||||
$expectedorder = [
|
||||
$qcat1->id => [],
|
||||
$qcat2->id => [],
|
||||
$qcat3->id => [],
|
||||
];
|
||||
$this->assertEquals($expectedorder, $currentorder);
|
||||
|
||||
// Move category 3 to the top.
|
||||
$stateupdates = move_category::execute($context->id, $qcat3->id, $qcat3->parent);
|
||||
|
||||
$neworder = $this->get_current_order($context);
|
||||
$newexpectedorder = [
|
||||
$qcat3->id => [],
|
||||
$qcat1->id => [],
|
||||
$qcat2->id => [],
|
||||
];
|
||||
$this->assertEquals($newexpectedorder, $neworder);
|
||||
|
||||
// Expecting all categories to have an updated sortorder.
|
||||
$expectedstateupdates = [
|
||||
(object)[
|
||||
'name' => 'categories',
|
||||
'action' => 'put',
|
||||
'fields' => (object)[
|
||||
'id' => $qcat3->id,
|
||||
'sortorder' => 1,
|
||||
],
|
||||
],
|
||||
(object)[
|
||||
'name' => 'categories',
|
||||
'action' => 'put',
|
||||
'fields' => (object)[
|
||||
'id' => $qcat1->id,
|
||||
'sortorder' => 2,
|
||||
],
|
||||
],
|
||||
(object)[
|
||||
'name' => 'categories',
|
||||
'action' => 'put',
|
||||
'fields' => (object)[
|
||||
'id' => $qcat2->id,
|
||||
'sortorder' => 3,
|
||||
],
|
||||
],
|
||||
];
|
||||
$this->assertEquals($stateupdates, $expectedstateupdates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a category to a new parent that doesn't currently have any children.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test_move_category_as_new_child(): void {
|
||||
$this->setAdminUser();
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Create context for question categories.
|
||||
$course = $this->create_course();
|
||||
$context = context_course::instance($course->id);
|
||||
$this->create_course_category();
|
||||
|
||||
// Question categories.
|
||||
$qcat1 = $this->create_question_category_for_a_course($course);
|
||||
$qcat2 = $this->create_question_category_for_a_course($course);
|
||||
$qcat3 = $this->create_question_category_for_a_course($course);
|
||||
|
||||
// Check current order.
|
||||
$currentorder = $this->get_current_order($context);
|
||||
$expectedorder = [
|
||||
$qcat1->id => [],
|
||||
$qcat2->id => [],
|
||||
$qcat3->id => [],
|
||||
];
|
||||
$this->assertEquals($expectedorder, $currentorder);
|
||||
|
||||
// Set Category 2 as the parent of Category 1.
|
||||
$stateupdates = move_category::execute($context->id, $qcat1->id, $qcat2->id);
|
||||
|
||||
$neworder = $this->get_current_order($context);
|
||||
$newexpectedorder = [
|
||||
$qcat2->id => [
|
||||
$qcat1->id => [],
|
||||
],
|
||||
$qcat3->id => [],
|
||||
];
|
||||
$this->assertEquals($newexpectedorder, $neworder);
|
||||
|
||||
// Expecting an update to the parent and sortorder of the moved category, and updated sortorders for the children
|
||||
// of the original parent.
|
||||
$expectedstateupdates = [
|
||||
(object)[
|
||||
'name' => 'categories',
|
||||
'action' => 'put',
|
||||
'fields' => (object)[
|
||||
'id' => $qcat1->id,
|
||||
'sortorder' => 1,
|
||||
'parent' => $qcat2->id,
|
||||
],
|
||||
],
|
||||
(object)[
|
||||
'name' => 'categories',
|
||||
'action' => 'put',
|
||||
'fields' => (object)[
|
||||
'id' => $qcat2->id,
|
||||
'sortorder' => 1,
|
||||
],
|
||||
],
|
||||
(object)[
|
||||
'name' => 'categories',
|
||||
'action' => 'put',
|
||||
'fields' => (object)[
|
||||
'id' => $qcat3->id,
|
||||
'sortorder' => 2,
|
||||
],
|
||||
],
|
||||
];
|
||||
$this->assertEquals($stateupdates, $expectedstateupdates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a category from one parent to another that already has a child.
|
||||
*
|
||||
* @return void
|
||||
* @throws \moodle_exception
|
||||
*/
|
||||
public function test_move_category_between_parents(): void {
|
||||
$this->setAdminUser();
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Create context for question categories.
|
||||
$course = $this->create_course();
|
||||
$context = context_course::instance($course->id);
|
||||
$this->create_course_category();
|
||||
|
||||
// Question categories.
|
||||
$qcat1 = $this->create_question_category_for_a_course($course);
|
||||
$qcat2 = $this->create_question_category_for_a_course($course);
|
||||
$qcat3 = $this->create_question_category_for_a_course($course, ['parent' => $qcat1->id]);
|
||||
$qcat4 = $this->create_question_category_for_a_course($course, ['parent' => $qcat2->id]);
|
||||
|
||||
// Check current order.
|
||||
$currentorder = $this->get_current_order($context);
|
||||
$expectedorder = [
|
||||
$qcat1->id => [
|
||||
$qcat3->id => [],
|
||||
],
|
||||
$qcat2->id => [
|
||||
$qcat4->id => [],
|
||||
],
|
||||
];
|
||||
$this->assertEquals($expectedorder, $currentorder);
|
||||
|
||||
// Set Category 2 as the parent of Category 1.
|
||||
$stateupdates = move_category::execute($context->id, $qcat3->id, $qcat2->id, $qcat4->id);
|
||||
|
||||
$neworder = $this->get_current_order($context);
|
||||
$newexpectedorder = [
|
||||
$qcat1->id => [],
|
||||
$qcat2->id => [
|
||||
$qcat4->id => [],
|
||||
$qcat3->id => [],
|
||||
],
|
||||
];
|
||||
$this->assertEquals($newexpectedorder, $neworder);
|
||||
|
||||
// As there are no remaining children of the original parent, and this was moved to the bottom of the new parent,
|
||||
// just the moved category is updated.
|
||||
$expectedstateupdates = [
|
||||
(object)[
|
||||
'name' => 'categories',
|
||||
'action' => 'put',
|
||||
'fields' => (object)[
|
||||
'id' => $qcat3->id,
|
||||
'sortorder' => 2,
|
||||
'parent' => $qcat2->id,
|
||||
],
|
||||
],
|
||||
];
|
||||
$this->assertEquals($stateupdates, $expectedstateupdates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a category from the top category of one context to another.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test_move_category_between_contexts(): void {
|
||||
$this->setAdminUser();
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Create context for question categories.
|
||||
$course = $this->create_course();
|
||||
$coursecontext = context_course::instance($course->id);
|
||||
$coursecategory = $this->create_course_category();
|
||||
$categorycontext = \context_coursecat::instance($coursecategory->id);
|
||||
|
||||
// Question categories.
|
||||
$coursecatqcat = question_get_top_category($categorycontext->id, true);
|
||||
$qcat1 = $this->create_question_category_for_a_course($course);
|
||||
$qcat2 = $this->create_question_category_for_a_course($course);
|
||||
|
||||
$currentcourseorder = $this->get_current_order($coursecontext);
|
||||
$expectedorder = [
|
||||
$qcat1->id => [],
|
||||
$qcat2->id => [],
|
||||
];
|
||||
$this->assertEquals($expectedorder, $currentcourseorder);
|
||||
$currentcoursecatorder = $this->get_current_order($categorycontext);
|
||||
$expectedcoursecatorder = [
|
||||
];
|
||||
$this->assertEquals($expectedcoursecatorder, $currentcoursecatorder);
|
||||
|
||||
$stateupdates = move_category::execute($coursecontext->id, $qcat1->id, $coursecatqcat->id);
|
||||
|
||||
$newcourseorder = $this->get_current_order($coursecontext);
|
||||
$newexpectedorder = [
|
||||
$qcat2->id => [],
|
||||
];
|
||||
$this->assertEquals($newcourseorder, $newexpectedorder);
|
||||
$newcoursecatorder = $this->get_current_order($categorycontext);
|
||||
$newexpectedcoursecatorder = [
|
||||
$qcat1->id => [],
|
||||
];
|
||||
$this->assertEquals($newcoursecatorder, $newexpectedcoursecatorder);
|
||||
|
||||
// Expect an update to the sortorder, parent and context of the moved category.
|
||||
// Since the original sibling is the only remaining child of a top-level category, it has its draghandle property
|
||||
// updated as well as its sortorder.
|
||||
$expectedstateupdates = [
|
||||
(object)[
|
||||
'name' => 'categories',
|
||||
'action' => 'put',
|
||||
'fields' => (object)[
|
||||
'id' => $qcat1->id,
|
||||
'sortorder' => 1,
|
||||
'parent' => $coursecatqcat->id,
|
||||
'context' => $categorycontext->id,
|
||||
],
|
||||
],
|
||||
(object)[
|
||||
'name' => 'categories',
|
||||
'action' => 'put',
|
||||
'fields' => (object)[
|
||||
'id' => $qcat2->id,
|
||||
'sortorder' => 1,
|
||||
'draghandle' => false,
|
||||
],
|
||||
],
|
||||
];
|
||||
$this->assertEquals($stateupdates, $expectedstateupdates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a category that has its own children.
|
||||
*
|
||||
* The children should move with the parent.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test_move_category_with_children(): void {
|
||||
$this->setAdminUser();
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Create context for question categories.
|
||||
$course = $this->create_course();
|
||||
$context = context_course::instance($course->id);
|
||||
$this->create_course_category();
|
||||
|
||||
// Question categories.
|
||||
$qcat1 = $this->create_question_category_for_a_course($course);
|
||||
$qcat2 = $this->create_question_category_for_a_course($course);
|
||||
$qcat3 = $this->create_question_category_for_a_course($course);
|
||||
$qcat4 = $this->create_question_category_for_a_course($course, ['parent' => $qcat3->id]);
|
||||
$qcat5 = $this->create_question_category_for_a_course($course, ['parent' => $qcat3->id]);
|
||||
|
||||
// Check current order.
|
||||
$currentorder = $this->get_current_order($context);
|
||||
$expectedorder = [
|
||||
$qcat1->id => [],
|
||||
$qcat2->id => [],
|
||||
$qcat3->id => [
|
||||
$qcat4->id => [],
|
||||
$qcat5->id => [],
|
||||
],
|
||||
];
|
||||
$this->assertEquals($expectedorder, $currentorder);
|
||||
|
||||
$stateupdates = move_category::execute($context->id, $qcat3->id, $qcat3->parent, $qcat1->id);
|
||||
|
||||
$neworder = $this->get_current_order($context);
|
||||
$newexpectedorder = [
|
||||
$qcat1->id => [],
|
||||
$qcat3->id => [
|
||||
$qcat4->id => [],
|
||||
$qcat5->id => [],
|
||||
],
|
||||
$qcat2->id => [],
|
||||
];
|
||||
$this->assertEquals($neworder, $newexpectedorder);
|
||||
|
||||
// Update the sortorder of the moving category and the following sibling. No updates to the children are required.
|
||||
$expectedstateupdates = [
|
||||
(object)[
|
||||
'name' => 'categories',
|
||||
'action' => 'put',
|
||||
'fields' => (object)[
|
||||
'id' => $qcat3->id,
|
||||
'sortorder' => 2,
|
||||
],
|
||||
],
|
||||
(object)[
|
||||
'name' => 'categories',
|
||||
'action' => 'put',
|
||||
'fields' => (object)[
|
||||
'id' => $qcat2->id,
|
||||
'sortorder' => 3,
|
||||
],
|
||||
],
|
||||
];
|
||||
$this->assertEquals($stateupdates, $expectedstateupdates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a category that has its own children to a new parent.
|
||||
*
|
||||
* The children should also move and become descendants of the new parent.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test_change_parent_with_children(): void {
|
||||
$this->setAdminUser();
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Create context for question categories.
|
||||
$course = $this->create_course();
|
||||
$context = context_course::instance($course->id);
|
||||
$this->create_course_category();
|
||||
|
||||
// Question categories.
|
||||
$qcat1 = $this->create_question_category_for_a_course($course);
|
||||
$qcat2 = $this->create_question_category_for_a_course($course);
|
||||
$qcat3 = $this->create_question_category_for_a_course($course);
|
||||
$qcat4 = $this->create_question_category_for_a_course($course, ['parent' => $qcat3->id]);
|
||||
$qcat5 = $this->create_question_category_for_a_course($course, ['parent' => $qcat4->id]);
|
||||
|
||||
// Check current order.
|
||||
$currentorder = $this->get_current_order($context);
|
||||
$expectedorder = [
|
||||
$qcat1->id => [],
|
||||
$qcat2->id => [],
|
||||
$qcat3->id => [
|
||||
$qcat4->id => [
|
||||
$qcat5->id => [],
|
||||
],
|
||||
],
|
||||
];
|
||||
$this->assertEquals($expectedorder, $currentorder);
|
||||
|
||||
$stateupdates = move_category::execute($context->id, $qcat4->id, $qcat2->id);
|
||||
|
||||
$neworder = $this->get_current_order($context);
|
||||
$newexpectedorder = [
|
||||
$qcat1->id => [],
|
||||
$qcat2->id => [
|
||||
$qcat4->id => [
|
||||
$qcat5->id => [],
|
||||
],
|
||||
],
|
||||
$qcat3->id => [],
|
||||
];
|
||||
$this->assertEquals($newexpectedorder, $neworder);
|
||||
|
||||
// Expecting an update to the sortorder and parent of the moved category. No updates to the children are required.
|
||||
$expectedstateupdates = [
|
||||
(object)[
|
||||
'name' => 'categories',
|
||||
'action' => 'put',
|
||||
'fields' => (object)[
|
||||
'id' => $qcat4->id,
|
||||
'parent' => $qcat2->id,
|
||||
'sortorder' => 1,
|
||||
],
|
||||
],
|
||||
];
|
||||
$this->assertEquals($expectedstateupdates, $stateupdates);
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace qbank_managecategories;
|
||||
|
||||
use core_question\local\bank\question_edit_contexts;
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace qbank_managecategories\privacy;
|
||||
|
||||
use advanced_testcase;
|
||||
use core_privacy\local\request\writer;
|
||||
use qbank_managecategories\privacy\provider;
|
||||
|
||||
/**
|
||||
* Unit tests for qbank_managecategories privacy provider.
|
||||
*
|
||||
* @package qbank_managecategories
|
||||
* @copyright 2021 Catalyst IT Australia Pty Ltd
|
||||
* @author 2021, Ghaly Marc-Alexandre <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @coversDefaultClass \qbank_managecategories\privacy\provider
|
||||
*/
|
||||
final class provider_test extends advanced_testcase {
|
||||
/**
|
||||
* Test to check export_user_preferences.
|
||||
*
|
||||
* @covers ::export_user_preferences
|
||||
*/
|
||||
public function test_export_user_preferences(): void {
|
||||
$this->resetAfterTest();
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
set_user_preference('qbank_managecategories_showdescriptions', 1, $user);
|
||||
provider::export_user_preferences($user->id);
|
||||
$writer = writer::with_context(\context_system::instance());
|
||||
$prefs = $writer->get_user_preferences('qbank_managecategories');
|
||||
$this->assertEquals(1, $prefs->showdescr->value);
|
||||
$this->assertEquals(get_string('displaydescription', 'qbank_managecategories'), $prefs->showdescr->description);
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace qbank_managecategories;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
@@ -29,7 +30,7 @@ require_once($CFG->dirroot . '/question/bank/managecategories/tests/manage_categ
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @covers \qbank_managecategories\question_categories
|
||||
*/
|
||||
class question_categories_test extends manage_category_test_base {
|
||||
final class question_categories_test extends manage_category_test_base {
|
||||
/**
|
||||
* Test creation of an ordered tree of categories in the constructor.
|
||||
*/
|
||||
|
||||
@@ -26,6 +26,6 @@
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$plugin->component = 'qbank_managecategories';
|
||||
$plugin->version = 2024042200;
|
||||
$plugin->version = 2024070400;
|
||||
$plugin->requires = 2024041600;
|
||||
$plugin->maturity = MATURITY_STABLE;
|
||||
|
||||
Reference in New Issue
Block a user