first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+10
View File
@@ -0,0 +1,10 @@
define("core_question/question_engine",["exports","core/scroll_manager","core_form/submit"],(function(_exports,scrollManager,formSubmit){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}
/**
* JavaScript required by the question engine.
*
* @module core_question/question_engine
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.preventRepeatSubmission=_exports.initSubmitButton=_exports.initForm=void 0,scrollManager=_interopRequireWildcard(scrollManager),formSubmit=_interopRequireWildcard(formSubmit);_exports.initSubmitButton=button=>{formSubmit.init(button),scrollManager.watchScrollButtonSaves()};_exports.initForm=formSelector=>{const form=document.querySelector(formSelector);form.setAttribute("autocomplete","off"),form.addEventListener("submit",preventRepeatSubmission),form.addEventListener("key",(event=>{13===event.keyCode&&(event.target.matches("a")||event.target.matches('input[type="submit"]')||event.target.matches("input[type=img]")||event.target.matches("textarea")||event.target.matches("[contenteditable=true]")||event.preventDefault())}));[...form.querySelectorAll(".questionflagsavebutton")].forEach((node=>node.remove())),scrollManager.scrollToSavedPosition()};const preventRepeatSubmission=event=>{const form=event.target.closest("form");"1"!==form.dataset.formSubmitted?(setTimeout((()=>{[...form.querySelectorAll("input[type=submit]")].forEach((input=>input.setAttribute("disabled",!0)))})),form.dataset.formSubmitted="1"):event.preventDefault()};_exports.preventRepeatSubmission=preventRepeatSubmission}));
//# sourceMappingURL=question_engine.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"question_engine.min.js","sources":["../src/question_engine.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 * JavaScript required by the question engine.\n *\n * @module core_question/question_engine\n * @copyright 2021 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport * as scrollManager from 'core/scroll_manager';\nimport * as formSubmit from 'core_form/submit';\n\n/**\n * Initialise a question submit button. This saves the scroll position and\n * sets the fragment on the form submit URL so the page reloads in the right place.\n *\n * @param {string} button the id of the button in the HTML.\n */\nexport const initSubmitButton = button => {\n formSubmit.init(button);\n scrollManager.watchScrollButtonSaves();\n};\n\n/**\n * Initialise a form that contains questions printed using print_question.\n * This has the effect of:\n * 1. Turning off browser autocomlete.\n * 2. Stopping enter from submitting the form (or toggling the next flag) unless\n * keyboard focus is on the submit button or the flag.\n * 3. Removes any '.questionflagsavebutton's, since we have JavaScript to toggle\n * the flags using ajax.\n * 4. Scroll to the position indicated by scrollpos= in the URL, if it is there.\n * 5. Prevent the user from repeatedly submitting the form.\n *\n * @param {string} formSelector Selector to identify the form.\n */\nexport const initForm = (formSelector) => {\n const form = document.querySelector(formSelector);\n form.setAttribute('autocomplete', 'off');\n\n form.addEventListener('submit', preventRepeatSubmission);\n\n form.addEventListener('key', (event) => {\n if (event.keyCode !== 13) {\n return;\n }\n\n if (event.target.matches('a')) {\n return;\n }\n\n if (event.target.matches('input[type=\"submit\"]')) {\n return;\n }\n\n if (event.target.matches('input[type=img]')) {\n return;\n }\n\n if (event.target.matches('textarea') || event.target.matches('[contenteditable=true]')) {\n return;\n }\n\n event.preventDefault();\n });\n\n const questionFlagSaveButtons = form.querySelectorAll('.questionflagsavebutton');\n [...questionFlagSaveButtons].forEach((node) => node.remove());\n\n // Note: The scrollToSavedPosition function tries to wait until the content has loaded before firing.\n scrollManager.scrollToSavedPosition();\n};\n\n/**\n * Event handler to stop a question form being submitted more than once.\n *\n * @param {object} event the form submit event.\n */\nexport const preventRepeatSubmission = (event) => {\n const form = event.target.closest('form');\n if (form.dataset.formSubmitted === '1') {\n event.preventDefault();\n return;\n }\n\n setTimeout(() => {\n [...form.querySelectorAll('input[type=submit]')].forEach((input) => input.setAttribute('disabled', true));\n });\n form.dataset.formSubmitted = '1';\n};\n"],"names":["button","formSubmit","init","scrollManager","watchScrollButtonSaves","formSelector","form","document","querySelector","setAttribute","addEventListener","preventRepeatSubmission","event","keyCode","target","matches","preventDefault","querySelectorAll","forEach","node","remove","scrollToSavedPosition","closest","dataset","formSubmitted","setTimeout","input"],"mappings":";;;;;;;+QAgCgCA,SAC5BC,WAAWC,KAAKF,QAChBG,cAAcC,4CAgBOC,qBACfC,KAAOC,SAASC,cAAcH,cACpCC,KAAKG,aAAa,eAAgB,OAElCH,KAAKI,iBAAiB,SAAUC,yBAEhCL,KAAKI,iBAAiB,OAAQE,QACJ,KAAlBA,MAAMC,UAIND,MAAME,OAAOC,QAAQ,MAIrBH,MAAME,OAAOC,QAAQ,yBAIrBH,MAAME,OAAOC,QAAQ,oBAIrBH,MAAME,OAAOC,QAAQ,aAAeH,MAAME,OAAOC,QAAQ,2BAI7DH,MAAMI,yBAGsBV,KAAKW,iBAAiB,4BACzBC,SAASC,MAASA,KAAKC,WAGpDjB,cAAckB,+BAQLV,wBAA2BC,cAC9BN,KAAOM,MAAME,OAAOQ,QAAQ,QACC,MAA/BhB,KAAKiB,QAAQC,eAKjBC,YAAW,SACHnB,KAAKW,iBAAiB,uBAAuBC,SAASQ,OAAUA,MAAMjB,aAAa,YAAY,QAEvGH,KAAKiB,QAAQC,cAAgB,KAPzBZ,MAAMI"}
+10
View File
@@ -0,0 +1,10 @@
define("core_question/refresh_ui",["exports","core/fragment","core/templates"],(function(_exports,_fragment,_templates){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
/**
* Question bank UI refresh utility
*
* @module core_question/refresh_ui
* @copyright 2023 Catalyst IT Europe Ltd.
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_fragment=_interopRequireDefault(_fragment),_templates=_interopRequireDefault(_templates);var _default={refresh:(uiRoot,returnUrl)=>new Promise(((resolve,reject)=>{const fragmentData=uiRoot.dataset,viewData={},sortData={};returnUrl&&returnUrl.searchParams.forEach(((value,key)=>{const sortItem=key.match(/sortdata\[([^\]]+)\]/);sortItem?sortData[sortItem.pop()]=value:viewData[key]=value})),viewData.sortdata=JSON.stringify(sortData),_fragment.default.loadFragment(fragmentData.component,fragmentData.callback,fragmentData.contextid,viewData).then(((html,js)=>(_templates.default.replaceNode(uiRoot,html,js),resolve(),html))).catch(reject)}))};return _exports.default=_default,_exports.default}));
//# sourceMappingURL=refresh_ui.min.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"refresh_ui.min.js","sources":["../src/refresh_ui.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 * Question bank UI refresh utility\n *\n * @module core_question/refresh_ui\n * @copyright 2023 Catalyst IT Europe Ltd.\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Fragment from 'core/fragment';\nimport Templates from 'core/templates';\n\nexport default {\n /**\n * Reload the question bank UI, retaining the current filters and sort data.\n *\n * @param {Element} uiRoot The root element of the UI to be refreshed. Must contain \"component\", \"callback\" and \"contextid\" in\n * its data attributes, to be passed to the Fragment API.\n * @param {URL} returnUrl The url of the current page, containing filter and sort parameters.\n * @return {Promise} Resolved when the refresh is complete.\n */\n refresh: (uiRoot, returnUrl) => {\n return new Promise((resolve, reject) => {\n const fragmentData = uiRoot.dataset;\n const viewData = {};\n const sortData = {};\n if (returnUrl) {\n returnUrl.searchParams.forEach((value, key) => {\n // Match keys like 'sortdata[fieldname]' and convert them to an array,\n // because the fragment API doesn't like non-alphanum argument keys.\n const sortItem = key.match(/sortdata\\[([^\\]]+)\\]/);\n if (sortItem) {\n // The item returned by sortItem.pop() is the contents of the matching group, the field name.\n sortData[sortItem.pop()] = value;\n } else {\n viewData[key] = value;\n }\n });\n }\n viewData.sortdata = JSON.stringify(sortData);\n // We have to use then() there, as loadFragment doesn't appear to work with await.\n Fragment.loadFragment(fragmentData.component, fragmentData.callback, fragmentData.contextid, viewData)\n .then((html, js) => {\n Templates.replaceNode(uiRoot, html, js);\n resolve();\n return html;\n })\n .catch(reject);\n });\n }\n};\n"],"names":["refresh","uiRoot","returnUrl","Promise","resolve","reject","fragmentData","dataset","viewData","sortData","searchParams","forEach","value","key","sortItem","match","pop","sortdata","JSON","stringify","loadFragment","component","callback","contextid","then","html","js","replaceNode","catch"],"mappings":";;;;;;;4LA0Be,CASXA,QAAS,CAACC,OAAQC,YACP,IAAIC,SAAQ,CAACC,QAASC,gBACnBC,aAAeL,OAAOM,QACtBC,SAAW,GACXC,SAAW,GACbP,WACAA,UAAUQ,aAAaC,SAAQ,CAACC,MAAOC,aAG7BC,SAAWD,IAAIE,MAAM,wBACvBD,SAEAL,SAASK,SAASE,OAASJ,MAE3BJ,SAASK,KAAOD,SAI5BJ,SAASS,SAAWC,KAAKC,UAAUV,4BAE1BW,aAAad,aAAae,UAAWf,aAAagB,SAAUhB,aAAaiB,UAAWf,UACxFgB,MAAK,CAACC,KAAMC,yBACCC,YAAY1B,OAAQwB,KAAMC,IACpCtB,UACOqB,QAEVG,MAAMvB"}
+259
View File
@@ -0,0 +1,259 @@
// 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/>.
/**
* Question bank filter management.
*
* @module core_question/filter
* @copyright 2021 Tomo Tsuyuki <tomotsuyuki@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import CoreFilter from 'core/datafilter';
import Notification from 'core/notification';
import Selectors from 'core/datafilter/selectors';
import Templates from 'core/templates';
import Fragment from 'core/fragment';
/**
* Initialise the question bank filter on the element with the given id.
*
* @param {String} filterRegionId ID of the HTML element containing the filters.
* @param {String} defaultcourseid Course ID for the default course to pass back to the view.
* @param {String} defaultcategoryid Question bank category ID for the default course to pass back to the view.
* @param {Number} perpage The number of questions to display per page.
* @param {Number} contextId Context ID of the question bank view.
* @param {string} component Frankenstyle name of the component for the fragment API callback (e.g. core_question)
* @param {string} callback Name of the callback for the fragment API (e.g question_data)
* @param {string} view The class name of the question bank view class used for this page.
* @param {Number} cmid If we are in an activitiy, the course module ID.
* @param {string} pagevars JSON-encoded parameters from passed from the view, including filters and jointype.
* @param {string} extraparams JSON-encoded additional parameters specific to this view class, used for re-rendering the view.
*/
export const init = (
filterRegionId,
defaultcourseid,
defaultcategoryid,
perpage,
contextId,
component,
callback,
view,
cmid,
pagevars,
extraparams
) => {
const SELECTORS = {
QUESTION_CONTAINER_ID: '#questionscontainer',
QUESTION_TABLE: '#questionscontainer table',
SORT_LINK: '#questionscontainer div.sorters a',
PAGINATION_LINK: '#questionscontainer a[href].page-link',
LASTCHANGED_FIELD: '#questionsubmit input[name=lastchanged]',
BULK_ACTIONS: '#bulkactionsui-container input',
MENU_ACTIONS: '.menu-action',
EDIT_SWITCH: '.editmode-switch-form input[name=setmode]',
EDIT_SWITCH_URL: '.editmode-switch-form input[name=pageurl]',
};
const filterSet = document.querySelector(`#${filterRegionId}`);
const viewData = {
extraparams,
cmid,
view,
cat: defaultcategoryid,
courseid: defaultcourseid,
filter: {},
jointype: 0,
qpage: 0,
qperpage: perpage,
sortdata: {},
lastchanged: document.querySelector(SELECTORS.LASTCHANGED_FIELD)?.value ?? null,
};
let sortData = {};
const defaultSort = document.querySelector(SELECTORS.QUESTION_TABLE)?.dataset?.defaultsort;
if (defaultSort) {
sortData = JSON.parse(defaultSort);
}
/**
* Retrieve table data.
*
* @param {Object} filterdata data
* @param {Promise} pendingPromise pending promise
*/
const applyFilter = (filterdata, pendingPromise) => {
// Reload the questions based on the specified filters. If no filters are provided,
// use the default category filter condition.
if (filterdata) {
// Main join types.
viewData.jointype = parseInt(filterSet.dataset.filterverb, 10);
delete filterdata.jointype;
// Retrieve filter info.
viewData.filter = filterdata;
if (Object.keys(filterdata).length !== 0) {
if (!isNaN(viewData.jointype)) {
filterdata.jointype = viewData.jointype;
}
updateUrlParams(filterdata);
}
}
// Load questions for first page.
viewData.filter = JSON.stringify(filterdata);
viewData.sortdata = JSON.stringify(sortData);
Fragment.loadFragment(component, callback, contextId, viewData)
// Render questions for first page and pagination.
.then((questionhtml, jsfooter) => {
const questionscontainer = document.querySelector(SELECTORS.QUESTION_CONTAINER_ID);
if (questionhtml === undefined) {
questionhtml = '';
}
if (jsfooter === undefined) {
jsfooter = '';
}
Templates.replaceNode(questionscontainer, questionhtml, jsfooter);
// Resolve filter promise.
if (pendingPromise) {
pendingPromise.resolve();
}
return {questionhtml, jsfooter};
})
.catch(Notification.exception);
};
// Init core filter processor with apply callback.
const coreFilter = new CoreFilter(filterSet, applyFilter);
coreFilter.activeFilters = {}; // Unset useless courseid filter.
coreFilter.init();
/**
* Update URL Param based upon the current filter.
*
* @param {Object} filters Active filters.
*/
const updateUrlParams = (filters) => {
const url = new URL(location.href);
const filterQuery = JSON.stringify(filters);
url.searchParams.set('filter', filterQuery);
history.pushState(filters, '', url);
const editSwitch = document.querySelector(SELECTORS.EDIT_SWITCH);
if (editSwitch) {
const editSwitchUrlInput = document.querySelector(SELECTORS.EDIT_SWITCH_URL);
const editSwitchUrl = new URL(editSwitchUrlInput.value);
editSwitchUrl.searchParams.set('filter', filterQuery);
editSwitchUrlInput.value = editSwitchUrl;
editSwitch.dataset.pageurl = editSwitchUrl;
}
};
/**
* Cleans URL parameters.
*/
const cleanUrlParams = () => {
const queryString = location.search;
const urlParams = new URLSearchParams(queryString);
if (urlParams.has('cmid')) {
const cleanedUrl = new URL(location.href.replace(location.search, ''));
cleanedUrl.searchParams.set('cmid', urlParams.get('cmid'));
history.pushState({}, '', cleanedUrl);
}
if (urlParams.has('courseid')) {
const cleanedUrl = new URL(location.href.replace(location.search, ''));
cleanedUrl.searchParams.set('courseid', urlParams.get('courseid'));
history.pushState({}, '', cleanedUrl);
}
};
// Add listeners for the sorting, paging and clear actions.
document.addEventListener('click', e => {
const sortableLink = e.target.closest(SELECTORS.SORT_LINK);
const paginationLink = e.target.closest(SELECTORS.PAGINATION_LINK);
const clearLink = e.target.closest(Selectors.filterset.actions.resetFilters);
if (sortableLink) {
e.preventDefault();
const oldSort = sortData;
sortData = {};
sortData[sortableLink.dataset.sortname] = sortableLink.dataset.sortorder;
for (const sortname in oldSort) {
if (sortname !== sortableLink.dataset.sortname) {
sortData[sortname] = oldSort[sortname];
}
}
viewData.qpage = 0;
coreFilter.updateTableFromFilter();
}
if (paginationLink) {
e.preventDefault();
const paginationURL = new URL(paginationLink.getAttribute("href"));
const qpage = paginationURL.searchParams.get('qpage');
if (paginationURL.search !== null) {
viewData.qpage = qpage;
coreFilter.updateTableFromFilter();
}
}
if (clearLink) {
cleanUrlParams();
}
});
// Run apply filter at page load.
pagevars = JSON.parse(pagevars);
let initialFilters;
let jointype = null;
if (pagevars.filter) {
// Load initial filter based on page vars.
initialFilters = pagevars.filter;
if (pagevars.jointype) {
jointype = pagevars.jointype;
}
}
if (Object.entries(initialFilters).length !== 0) {
// Remove the default empty filter row.
const emptyFilterRow = filterSet.querySelector(Selectors.filterset.regions.emptyFilterRow);
if (emptyFilterRow) {
emptyFilterRow.remove();
}
// Add filters.
let rowcount = 0;
for (const urlFilter in initialFilters) {
if (urlFilter === 'jointype') {
jointype = initialFilters[urlFilter];
continue;
}
// Add each filter row.
rowcount += 1;
const filterdata = {
filtertype: urlFilter,
values: initialFilters[urlFilter].values,
jointype: initialFilters[urlFilter].jointype,
filteroptions: initialFilters[urlFilter].filteroptions,
rownum: rowcount
};
coreFilter.addFilterRow(filterdata);
}
coreFilter.filterSet.dataset.filterverb = jointype;
// Since we must filter by category, it does not make sense to allow the top-level "match any" or "match none" conditions,
// as this would exclude the category. Remove those options and disable the select.
const join = coreFilter.filterSet.querySelector(Selectors.filterset.fields.join);
join.querySelectorAll(`option:not([value="${jointype}"])`).forEach((option) => option.remove());
join.disabled = true;
}
};
+104
View File
@@ -0,0 +1,104 @@
// 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/>.
/**
* JavaScript required by the question engine.
*
* @module core_question/question_engine
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import * as scrollManager from 'core/scroll_manager';
import * as formSubmit from 'core_form/submit';
/**
* Initialise a question submit button. This saves the scroll position and
* sets the fragment on the form submit URL so the page reloads in the right place.
*
* @param {string} button the id of the button in the HTML.
*/
export const initSubmitButton = button => {
formSubmit.init(button);
scrollManager.watchScrollButtonSaves();
};
/**
* Initialise a form that contains questions printed using print_question.
* This has the effect of:
* 1. Turning off browser autocomlete.
* 2. Stopping enter from submitting the form (or toggling the next flag) unless
* keyboard focus is on the submit button or the flag.
* 3. Removes any '.questionflagsavebutton's, since we have JavaScript to toggle
* the flags using ajax.
* 4. Scroll to the position indicated by scrollpos= in the URL, if it is there.
* 5. Prevent the user from repeatedly submitting the form.
*
* @param {string} formSelector Selector to identify the form.
*/
export const initForm = (formSelector) => {
const form = document.querySelector(formSelector);
form.setAttribute('autocomplete', 'off');
form.addEventListener('submit', preventRepeatSubmission);
form.addEventListener('key', (event) => {
if (event.keyCode !== 13) {
return;
}
if (event.target.matches('a')) {
return;
}
if (event.target.matches('input[type="submit"]')) {
return;
}
if (event.target.matches('input[type=img]')) {
return;
}
if (event.target.matches('textarea') || event.target.matches('[contenteditable=true]')) {
return;
}
event.preventDefault();
});
const questionFlagSaveButtons = form.querySelectorAll('.questionflagsavebutton');
[...questionFlagSaveButtons].forEach((node) => node.remove());
// Note: The scrollToSavedPosition function tries to wait until the content has loaded before firing.
scrollManager.scrollToSavedPosition();
};
/**
* Event handler to stop a question form being submitted more than once.
*
* @param {object} event the form submit event.
*/
export const preventRepeatSubmission = (event) => {
const form = event.target.closest('form');
if (form.dataset.formSubmitted === '1') {
event.preventDefault();
return;
}
setTimeout(() => {
[...form.querySelectorAll('input[type=submit]')].forEach((input) => input.setAttribute('disabled', true));
});
form.dataset.formSubmitted = '1';
};
+65
View File
@@ -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/>.
/**
* Question bank UI refresh utility
*
* @module core_question/refresh_ui
* @copyright 2023 Catalyst IT Europe Ltd.
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Fragment from 'core/fragment';
import Templates from 'core/templates';
export default {
/**
* Reload the question bank UI, retaining the current filters and sort data.
*
* @param {Element} uiRoot The root element of the UI to be refreshed. Must contain "component", "callback" and "contextid" in
* its data attributes, to be passed to the Fragment API.
* @param {URL} returnUrl The url of the current page, containing filter and sort parameters.
* @return {Promise} Resolved when the refresh is complete.
*/
refresh: (uiRoot, returnUrl) => {
return new Promise((resolve, reject) => {
const fragmentData = uiRoot.dataset;
const viewData = {};
const sortData = {};
if (returnUrl) {
returnUrl.searchParams.forEach((value, key) => {
// Match keys like 'sortdata[fieldname]' and convert them to an array,
// because the fragment API doesn't like non-alphanum argument keys.
const sortItem = key.match(/sortdata\[([^\]]+)\]/);
if (sortItem) {
// The item returned by sortItem.pop() is the contents of the matching group, the field name.
sortData[sortItem.pop()] = value;
} else {
viewData[key] = value;
}
});
}
viewData.sortdata = JSON.stringify(sortData);
// We have to use then() there, as loadFragment doesn't appear to work with await.
Fragment.loadFragment(fragmentData.component, fragmentData.callback, fragmentData.contextid, viewData)
.then((html, js) => {
Templates.replaceNode(uiRoot, html, js);
resolve();
return html;
})
.catch(reject);
});
}
};
@@ -0,0 +1,47 @@
<?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_bulkmove;
/**
* Class bulk_move_action is the base class for moving questions.
*
* @package qbank_bulkmove
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class bulk_move_action extends \core_question\local\bank\bulk_action_base {
public function get_bulk_action_title(): string {
return get_string('movetobulkaction', 'qbank_bulkmove');
}
public function get_key(): string {
return 'move';
}
public function get_bulk_action_url(): \moodle_url {
return new \moodle_url('/question/bank/bulkmove/move.php');
}
public function get_bulk_action_capabilities(): ?array {
return [
'moodle/question:moveall',
'moodle/question:add',
];
}
}
+92
View File
@@ -0,0 +1,92 @@
<?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_bulkmove;
/**
* Bulk move helper.
*
* @package qbank_bulkmove
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class helper {
/**
* Bulk move questions to a category.
*
* @param string $movequestionselected comma separated string of questions to be moved.
* @param \stdClass $tocategory the category where the questions will be moved to.
*/
public static function bulk_move_questions(string $movequestionselected, \stdClass $tocategory): void {
global $DB;
if ($questionids = explode(',', $movequestionselected)) {
list($usql, $params) = $DB->get_in_or_equal($questionids);
$sql = "SELECT q.*, c.contextid
FROM {question} q
JOIN {question_versions} qv ON qv.questionid = q.id
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
JOIN {question_categories} c ON c.id = qbe.questioncategoryid
WHERE q.id
{$usql}";
$questions = $DB->get_records_sql($sql, $params);
foreach ($questions as $question) {
question_require_capability_on($question, 'move');
}
question_move_questions_to_category($questionids, $tocategory->id);
}
}
/**
* Get the display data for the move form.
*
* @param array $addcontexts the array of contexts to be considered in order to render the category select menu.
* @param \moodle_url $moveurl the url where the move script will point to.
* @param \moodle_url $returnurl return url in case the form is cancelled.
* @return array the data to be rendered in the mustache where it contains the dropdown, move url and return url.
*/
public static function get_displaydata(array $addcontexts, \moodle_url $moveurl, \moodle_url $returnurl): array {
$displaydata = [];
$displaydata ['categorydropdown'] = \qbank_managecategories\helper::question_category_select_menu($addcontexts,
false, 0, '', -1, true);
$displaydata ['moveurl'] = $moveurl;
$displaydata['returnurl'] = $returnurl;
return $displaydata;
}
/**
* Process the question came from the form post.
*
* @param array $rawquestions raw questions came as a part of post.
* @return array question ids got from the post are processed and structured in an array.
*/
public static function process_question_ids(array $rawquestions): array {
$questionids = [];
$questionlist = '';
foreach ($rawquestions as $key => $notused) {
// Parse input for question ids.
if (preg_match('!^q([0-9]+)$!', $key, $matches)) {
$key = $matches[1];
$questionids[] = $key;
}
}
if (!empty($questionids)) {
$questionlist = implode(',', $questionids);
}
return [$questionids, $questionlist];
}
}
@@ -0,0 +1,39 @@
<?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_bulkmove\output;
/**
* Class renderer.
*
* @package qbank_bulkmove
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class renderer extends \plugin_renderer_base {
/**
* Render bulk move.
*
* @param array $displaydata
* @return string
*/
public function render_bulk_move_form($displaydata) {
return $this->render_from_template('qbank_bulkmove/bulk_move', $displaydata);
}
}
@@ -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/>.
namespace qbank_bulkmove;
use core_question\local\bank\bulk_action_base;
use core_question\local\bank\plugin_features_base;
/**
* Class plugin_feature is the entrypoint for the features.
*
* @package qbank_bulkmove
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class plugin_feature extends plugin_features_base {
public function get_bulk_actions(): array {
return [
new bulk_move_action(),
];
}
}
@@ -0,0 +1,32 @@
<?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_bulkmove\privacy;
/**
* Privacy Subsystem for qbank_deletequestion implementing null_provider.
*
* @package qbank_bulkmove
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements \core_privacy\local\metadata\null_provider {
public static function get_reason(): string {
return 'privacy:metadata';
}
}
@@ -0,0 +1,31 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Strings for component qbank_bulkmove, language 'en'
*
* @package qbank_bulkmove
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['bulkmoveheader'] = 'Move the selected questions';
$string['close'] = 'Close';
$string['movequestions'] = 'Move questions';
$string['movetobulkaction'] = 'Move to...';
$string['pluginname'] = 'Bulk move questions';
$string['privacy:metadata'] = 'The Bulk move questions question bank plugin does not store any personal data.';
+110
View File
@@ -0,0 +1,110 @@
<?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/>.
/**
* Move questions page.
*
* @package qbank_bulkmove
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__ . '/../../../config.php');
require_once(__DIR__ . '/../../editlib.php');
global $DB, $OUTPUT, $PAGE, $COURSE;
$moveselected = optional_param('move', false, PARAM_BOOL);
$returnurl = optional_param('returnurl', 0, PARAM_LOCALURL);
$cmid = optional_param('cmid', 0, PARAM_INT);
$courseid = optional_param('courseid', 0, PARAM_INT);
$category = optional_param('category', null, PARAM_SEQUENCE);
$confirm = optional_param('confirm', '', PARAM_ALPHANUM);
$movequestionselected = optional_param('movequestionsselected', null, PARAM_RAW);
if ($returnurl) {
$returnurl = new moodle_url($returnurl);
}
\core_question\local\bank\helper::require_plugin_enabled('qbank_bulkmove');
if ($cmid) {
list($module, $cm) = get_module_from_cmid($cmid);
require_login($cm->course, false, $cm);
$thiscontext = context_module::instance($cmid);
} else if ($courseid) {
require_login($courseid, false);
$thiscontext = context_course::instance($courseid);
} else {
throw new moodle_exception('missingcourseorcmid', 'question');
}
$contexts = new core_question\local\bank\question_edit_contexts($thiscontext);
$url = new moodle_url('/question/bank/bulkmove/move.php');
$PAGE->set_url($url);
$streditingquestions = get_string('movequestions', 'qbank_bulkmove');
$PAGE->set_title($streditingquestions);
$PAGE->set_heading($COURSE->fullname);
$PAGE->activityheader->disable();
$PAGE->set_secondary_active_tab("questionbank");
if ($category) {
list($tocategoryid, $contextid) = explode(',', $category);
if (! $tocategory = $DB->get_record('question_categories',
['id' => $tocategoryid, 'contextid' => $contextid])) {
throw new \moodle_exception('cannotfindcate', 'question');
}
}
if ($movequestionselected && $confirm && confirm_sesskey()) {
if ($confirm == md5($movequestionselected)) {
\qbank_bulkmove\helper::bulk_move_questions($movequestionselected, $tocategory);
}
$returnfilters = \core_question\local\bank\filter_condition_manager::update_filter_param_to_category(
$returnurl->param('filter'),
$tocategoryid,
);
redirect(new moodle_url($returnurl, ['filter' => $returnfilters]));
}
echo $OUTPUT->header();
if ($moveselected) {
$rawquestions = $_REQUEST;
list($questionids, $questionlist) = \qbank_bulkmove\helper::process_question_ids($rawquestions);
// No questions were selected.
if (!$questionids) {
redirect($returnurl);
}
// Create the urls.
$moveparam = [
'movequestionsselected' => $questionlist,
'confirm' => md5($questionlist),
'sesskey' => sesskey(),
'returnurl' => $returnurl,
'cmid' => $cmid,
'courseid' => $courseid,
];
$moveurl = new \moodle_url($url, $moveparam);
$addcontexts = $contexts->having_cap('moodle/question:add');
$displaydata = \qbank_bulkmove\helper::get_displaydata($addcontexts, $moveurl, $returnurl);
echo $PAGE->get_renderer('qbank_bulkmove')->render_bulk_move_form($displaydata);
}
echo $OUTPUT->footer();
@@ -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_bulkmove/bulk_move
The move form to move selested questions.
Context variables required for this template:
* categorydropdown - dropdown html from the managecategories plugin for the list of categories
* moveurl - the url to post the selected category
* returnurl - the base page to return to
Example context (json):
{
"categorydropdown": "<select class='select custom-select custom-select'><optgroup label='Course: tes'><option value='2,13'>Default for test (5)</option></optgroup></select>",
"moveurl": "/question/bank/bulkmove/move.php?courseid=2",
"returnurl": "/question/edit.php?courseid=2"
}
}}
<div class="bulkmovequestion-header">
<h3>{{#str}} bulkmoveheader, qbank_bulkmove {{/str}}</h3>
</div>
<form action="{{{moveurl}}}" method="post" id="bulkmovequestion">
{{{categorydropdown}}}
<input type="submit" value="{{#str}} moveto, question {{/str}}" class="btn btn-primary" name="move" data-action="toggle" data-togglegroup="qbank"
data-toggle="action" form="bulkmovequestion">
<a href="{{{returnurl}}}" class="btn btn-secondary">{{#str}} close, qbank_bulkmove {{/str}}</a>
</form>
@@ -0,0 +1,34 @@
@qbank @qbank_bulkmove
Feature: Use the qbank plugin manager page for bulkmove
In order to check the plugin behaviour with enable and disable
Background:
Given the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "activities" exist:
| activity | name | course | idnumber |
| quiz | Test quiz | C1 | quiz1 |
And the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Test questions |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | First question | Answer the first question |
@javascript
Scenario: Enable/disable bulk move questions bulk action from the base view
Given I log in as "admin"
When I navigate to "Plugins > Question bank plugins > Manage question bank plugins" in site administration
And I should see "Bulk move questions"
And I click on "Disable" "link" in the "Bulk move questions" "table_row"
And I am on the "Test quiz" "mod_quiz > question bank" page
And I click on "First question" "checkbox"
And I click on "With selected" "button"
Then I should not see question bulk action "move"
And I navigate to "Plugins > Question bank plugins > Manage question bank plugins" in site administration
And I click on "Enable" "link" in the "Bulk move questions" "table_row"
And I am on the "Test quiz" "mod_quiz > question bank" page
And I click on "First question" "checkbox"
And I click on "With selected" "button"
And I should see question bulk action "move"
@@ -0,0 +1,224 @@
<?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_bulkmove;
use core_question\local\bank\question_edit_contexts;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/question/editlib.php');
/**
* Bulk move helper tests.
*
* @package qbank_bulkmove
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \qbank_bulkmove\helper
*/
class helper_test extends \advanced_testcase {
/**
* @var false|object|\stdClass|null $cat
*/
protected $cat;
/**
* @var \stdClass $questiondata1
*/
protected $questiondata1;
/**
* @var \stdClass $questiondata2
*/
protected $questiondata2;
/**
* @var bool|\context|\context_course $context
*/
protected $context;
/**
* @var \core_question\local\bank\question_edit_contexts $contexts
*/
protected $contexts;
/**
* @var \stdClass $course
*/
protected $course;
/**
* @var array $rawdata
*/
protected $rawdata;
/**
* @var object $secondcategory
*/
protected $secondcategory;
/**
* Setup the test.
*/
protected function helper_setup(): void {
$this->resetAfterTest();
$this->setAdminUser();
$generator = $this->getDataGenerator();
/** @var \core_question_generator $questiongenerator */
$questiongenerator = $generator->get_plugin_generator('core_question');
// Create a course.
$this->course = $generator->create_course();
$this->context = \context_course::instance($this->course->id);
// Create a question in the default category.
$this->contexts = new question_edit_contexts($this->context);
$this->cat = question_make_default_categories($this->contexts->all());
$this->questiondata1 = $questiongenerator->create_question('numerical', null,
['name' => 'Example question', 'category' => $this->cat->id]);
// Create a second category to move questions.
$this->secondcategory = $questiongenerator->create_question_category(['contextid' => $this->context->id,
'parent' => $this->cat->id]);
// Ensure the question is not in the cache.
$cache = \cache::make('core', 'questiondata');
$cache->delete($this->questiondata1->id);
$this->questiondata2 = $questiongenerator->create_question('numerical', null,
['name' => 'Example question second', 'category' => $this->cat->id]);
// Ensure the question is not in the cache.
$cache = \cache::make('core', 'questiondata');
$cache->delete($this->questiondata2->id);
// Posted raw data.
$this->rawdata = [
'courseid' => $this->course->id,
'cat' => "{$this->cat->id},{$this->context->id}",
'qpage' => '0',
"q{$this->questiondata1->id}" => '1',
"q{$this->questiondata2->id}" => '1',
'move' => 'Move to'
];
}
/**
* Count how many questions in the list belong to the given category.
*
* @param string $categoryid a category id
* @param array $questionids list of question ids
* @return int
*/
private function count_category_questions(string $categoryid, array $questionids): int {
global $DB;
$this->assertNotEmpty($questionids);
list($insql, $inparams) = $DB->get_in_or_equal($questionids, SQL_PARAMS_NAMED);
$sql = "SELECT COUNT(q.id)
FROM {question} q
JOIN {question_versions} qv ON qv.questionid = q.id
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
WHERE qc.id = :categoryid
AND q.id $insql";
return $DB->count_records_sql($sql, array_merge(['categoryid' => $categoryid], $inparams));
}
/**
* Assert that the given category contains following questions
*
* @param string $categoryid a category id
* @param array $questionids list of question ids
* @return void
*/
protected function assert_category_contains_questions(string $categoryid, array $questionids) {
// The category need to contain all the questions.
$this->assertEquals(count($questionids), $this->count_category_questions($categoryid, $questionids));
}
/**
* Assert that the given category does not contain following questions
*
* @param string $categoryid a category id
* @param array $questionids list of question ids
* @return void
*/
protected function assert_category_does_not_contain_questions(string $categoryid, array $questionids) {
// The category does not contain any question.
$this->assertEquals(0, $this->count_category_questions($categoryid, $questionids));
}
/**
* Test bulk move of questions.
*
* @covers ::bulk_move_questions
*/
public function test_bulk_move_questions(): void {
global $DB;
$this->helper_setup();
// Get the processed question ids.
$questionlist = $this->process_question_ids_test();
$questionids = array_map('intval', explode(',', $questionlist));
// Verify that the questions are available in the current view.
$this->assert_category_contains_questions($this->cat->id, $questionids);
helper::bulk_move_questions($questionlist, $this->secondcategory);
// Verify the questions are not in the current category.
$this->assert_category_does_not_contain_questions($this->cat->id, $questionids);
// Verify the questions are in the new category.
$this->assert_category_contains_questions($this->secondcategory->id, $questionids);
}
/**
* Test the question processing and return the question list.
*
* @return mixed
* @covers ::process_question_ids
*/
protected function process_question_ids_test() {
// Test the raw data processing.
list($questionids, $questionlist) = helper::process_question_ids($this->rawdata);
$this->assertEquals([$this->questiondata1->id, $this->questiondata2->id], $questionids);
$this->assertEquals("{$this->questiondata1->id},{$this->questiondata2->id}", $questionlist);
return $questionlist;
}
/**
* Test the question displaydata.
*
* @covers ::get_displaydata
*/
public function test_get_displaydata(): void {
$this->helper_setup();
$coursecontext = \context_course::instance($this->course->id);
$contexts = new question_edit_contexts($coursecontext);
$addcontexts = $contexts->having_cap('moodle/question:add');
$url = new \moodle_url('/question/bank/bulkmove/move.php');
$displaydata = \qbank_bulkmove\helper::get_displaydata($addcontexts, $url, $url);
$this->assertStringContainsString('Test question category 1', $displaydata['categorydropdown']);
$this->assertStringContainsString('Default for Category 1', $displaydata['categorydropdown']);
$this->assertEquals($url, $displaydata ['moveurl']);
$this->assertEquals($url, $displaydata ['returnurl']);
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Version information for qbank_bulkmove.
*
* @package qbank_bulkmove
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'qbank_bulkmove';
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
$plugin->maturity = MATURITY_STABLE;
+98
View File
@@ -0,0 +1,98 @@
<?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/>.
/**
* Actions controller
*
* Perform a synchronous action to modify the question bank UI and redirect back to the previous page.
* These features are mostly progressively enhanced by actions.js and web services, but this remains as a fallback.
*
* @package qbank_columnsortorder
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__ . '/../../../config.php');
$action = required_param('action', PARAM_TEXT);
$global = optional_param('global', false, PARAM_BOOL);
$returnurl = optional_param('returnurl', '/question/bank/columnsortorder/sortcolumns.php', PARAM_LOCALURL);
require_login();
if ($global) {
require_capability('moodle/site:config', context_system::instance());
}
if ($action === 'debugreset' && $CFG->debug === DEBUG_DEVELOPER) {
$columnmanager = new \qbank_columnsortorder\column_manager($global);
$columnmanager::set_hidden_columns([], $global);
$columnmanager::set_column_order([], $global);
$columnmanager::set_column_size('', $global);
redirect(new moodle_url($returnurl));
}
require_sesskey();
$columnmanager = new \qbank_columnsortorder\column_manager($global);
switch ($action) {
case 'add':
case 'remove':
$column = required_param('column', PARAM_RAW);
[$columnclass, ] = explode(\core_question\local\bank\column_base::ID_SEPARATOR, $column);
if (!class_exists($columnclass)) {
throw new invalid_parameter_exception("'{$columnclass}' is not a valid column class.");
}
$hiddencolumns = $columnmanager->hiddencolumns;
if ($action === 'add') {
$key = array_search($column, $hiddencolumns);
if ($key !== false) {
unset($hiddencolumns[$key]);
}
} else {
if (!in_array($column, $hiddencolumns)) {
$hiddencolumns[] = $column;
}
}
$columnmanager::set_hidden_columns($hiddencolumns, $global);
break;
case 'savewidths':
$rawwidths = optional_param_array('width', [], PARAM_INT);
$widths = [];
foreach (array_filter($rawwidths) as $escapedclass => $width) {
$class = str_replace('__', '\\', $escapedclass);
// Validate that the class exists and the width is valid.
// Since the browser uses Constraint Validation to prevent the form being submitted with an invalid width,
// the only way we'll get one here is if someone is messing around, so don't worry about re-displaying the
// form with an error message, just ignore the invalid value.
if (class_exists($class) && $width >= 10) {
$widths[] = (object)[
'column' => $class,
'width' => $width,
];
}
}
$columnmanager::set_column_size(json_encode($widths), $global);
break;
case 'reset':
$columnmanager::set_hidden_columns(null, $global);
$columnmanager::set_column_order(null, $global);
$columnmanager::set_column_size(null, $global);
break;
}
redirect(new moodle_url($returnurl));
+11
View File
@@ -0,0 +1,11 @@
define("qbank_columnsortorder/actions",["exports","core/sortable_list","jquery","qbank_columnsortorder/repository","core/notification","core_question/refresh_ui"],(function(_exports,_sortable_list,_jquery,repository,_notification,_refresh_ui){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
/**
* Common javascript for handling actions on the admin page and the user's view of the question bank.
*
* @module qbank_columnsortorder/actions
* @copyright 2023 onwards Catalyst IT Europe Ltd
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.setupSortableLists=_exports.setupActionButtons=_exports.getColumnOrder=_exports.SELECTORS=void 0,_sortable_list=_interopRequireDefault(_sortable_list),_jquery=_interopRequireDefault(_jquery),repository=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(repository),_notification=_interopRequireDefault(_notification),_refresh_ui=_interopRequireDefault(_refresh_ui);const SELECTORS={columnList:".qbank-column-list",sortableColumn:".qbank-sortable-column",removeLink:"[data-action=remove]",moveHandler:"[data-drag-type=move]",addColumn:".addcolumn",addLink:"[data-action=add]",actionLink:".action-link"};_exports.SELECTORS=SELECTORS;_exports.setupSortableLists=function(listRoot){let vertical=arguments.length>1&&void 0!==arguments[1]&&arguments[1],global=arguments.length>2&&void 0!==arguments[2]&&arguments[2];const sortableList=new _sortable_list.default(listRoot,{moveHandlerSelector:SELECTORS.moveHandler,isHorizontal:!vertical});sortableList.getElementName=element=>Promise.resolve(element.data("name"));const sortableColumns=(0,_jquery.default)(SELECTORS.sortableColumn);return sortableColumns.on(_sortable_list.default.EVENTS.DROP,(()=>{repository.setColumnbankOrder(getColumnOrder(listRoot),global).catch(_notification.default.exception),listRoot.querySelectorAll(SELECTORS.sortableColumn).forEach((item=>item.classList.remove("active")))})),sortableColumns.on(_sortable_list.default.EVENTS.DRAGSTART,(event=>{event.currentTarget.classList.add("active")})),sortableColumns};_exports.setupActionButtons=function(uiRoot){let global=arguments.length>1&&void 0!==arguments[1]&&arguments[1];uiRoot.addEventListener("click",(async e=>{const actionLink=e.target.closest(SELECTORS.actionLink);if(actionLink)try{e.preventDefault();const action=actionLink.dataset.action;if("add"===action||"remove"===action){const hiddenColumns=[],addColumnList=document.querySelector(SELECTORS.addColumn);addColumnList&&addColumnList.querySelectorAll(SELECTORS.addLink).forEach((item=>{"add"===action&&item===actionLink||hiddenColumns.push(item.dataset.column)})),"remove"===action&&hiddenColumns.push(actionLink.dataset.column),await repository.setHiddenColumns(hiddenColumns,global)}else"reset"===action&&await repository.resetColumns(global);const actionUrl=new URL(actionLink.href),returnUrl=new URL(actionUrl.searchParams.get("returnurl").replaceAll("&amp;","&"));await _refresh_ui.default.refresh(uiRoot,returnUrl)}catch(ex){await _notification.default.exception(ex)}}))};const getColumnOrder=listRoot=>{const columns=Array.from(listRoot.querySelectorAll("[data-columnid]")).map((column=>column.dataset.columnid));return columns.filter(((value,index)=>columns.indexOf(value)===index))};_exports.getColumnOrder=getColumnOrder}));
//# sourceMappingURL=actions.min.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,11 @@
define("qbank_columnsortorder/admin_actions",["exports","qbank_columnsortorder/actions","qbank_columnsortorder/repository","core/notification","core/pending"],(function(_exports,actions,repository,_notification,_pending){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}
/**
* Javascript for handling actions on the admin page
*
* @module qbank_columnsortorder/admin_actions
* @copyright 2023 onwards Catalyst IT Europe Ltd
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,actions=_interopRequireWildcard(actions),repository=_interopRequireWildcard(repository),_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending);_exports.init=id=>{const uiRoot=document.getElementById(id),listRoot=uiRoot.querySelector(actions.SELECTORS.columnList);actions.setupSortableLists(listRoot,!0,!0),actions.setupActionButtons(uiRoot,!0),(listRoot=>{listRoot.addEventListener("change",(async()=>{const pendingPromise=new _pending.default("saveWidths"),columns=listRoot.querySelectorAll(actions.SELECTORS.sortableColumn),widths=[];columns.forEach((column=>{const widthInput=column.querySelector(".width-input"),valid=widthInput.checkValidity();widthInput.closest(".has-validation").classList.add("was-validated"),valid&&widths.push({column:column.dataset.columnid,width:widthInput.value})})),await repository.setColumnSize(JSON.stringify(widths),!0).catch(_notification.default.exception),pendingPromise.resolve()}))})(listRoot)}}));
//# sourceMappingURL=admin_actions.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"admin_actions.min.js","sources":["../src/admin_actions.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 * Javascript for handling actions on the admin page\n *\n * @module qbank_columnsortorder/admin_actions\n * @copyright 2023 onwards Catalyst IT Europe Ltd\n * @author Mark Johnson <mark.johnson@catalyst-eu.net>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport * as actions from 'qbank_columnsortorder/actions';\nimport * as repository from 'qbank_columnsortorder/repository';\nimport Notification from \"core/notification\";\nimport Pending from 'core/pending';\n\n/**\n * Event handler to save the custom column widths when a field is edited.\n *\n * @param {Element} listRoot The root element of the list of columns.\n */\nconst setupSaveWidths = listRoot => {\n listRoot.addEventListener('change', async() => {\n const pendingPromise = new Pending('saveWidths');\n const columns = listRoot.querySelectorAll(actions.SELECTORS.sortableColumn);\n const widths = [];\n columns.forEach(column => {\n const widthInput = column.querySelector('.width-input');\n const valid = widthInput.checkValidity();\n widthInput.closest('.has-validation').classList.add('was-validated');\n if (!valid) {\n return;\n }\n widths.push({\n column: column.dataset.columnid,\n width: widthInput.value,\n });\n });\n await repository.setColumnSize(JSON.stringify(widths), true).catch(Notification.exception);\n pendingPromise.resolve();\n });\n};\n\n/**\n * Initialize module\n *\n * Set up event handlers for the action buttons, width fields and initialise column sorting.\n *\n * @param {String} id ID for the admin UI root element.\n */\nexport const init = id => {\n const uiRoot = document.getElementById(id);\n const listRoot = uiRoot.querySelector(actions.SELECTORS.columnList);\n actions.setupSortableLists(listRoot, true, true);\n actions.setupActionButtons(uiRoot, true);\n setupSaveWidths(listRoot);\n};\n"],"names":["id","uiRoot","document","getElementById","listRoot","querySelector","actions","SELECTORS","columnList","setupSortableLists","setupActionButtons","addEventListener","async","pendingPromise","Pending","columns","querySelectorAll","sortableColumn","widths","forEach","column","widthInput","valid","checkValidity","closest","classList","add","push","dataset","columnid","width","value","repository","setColumnSize","JSON","stringify","catch","Notification","exception","resolve","setupSaveWidths"],"mappings":";;;;;;;;sRA+DoBA,WACVC,OAASC,SAASC,eAAeH,IACjCI,SAAWH,OAAOI,cAAcC,QAAQC,UAAUC,YACxDF,QAAQG,mBAAmBL,UAAU,GAAM,GAC3CE,QAAQI,mBAAmBT,QAAQ,GAjCfG,CAAAA,WACpBA,SAASO,iBAAiB,UAAUC,gBAC1BC,eAAiB,IAAIC,iBAAQ,cAC7BC,QAAUX,SAASY,iBAAiBV,QAAQC,UAAUU,gBACtDC,OAAS,GACfH,QAAQI,SAAQC,eACNC,WAAaD,OAAOf,cAAc,gBAClCiB,MAAQD,WAAWE,gBACzBF,WAAWG,QAAQ,mBAAmBC,UAAUC,IAAI,iBAC/CJ,OAGLJ,OAAOS,KAAK,CACRP,OAAQA,OAAOQ,QAAQC,SACvBC,MAAOT,WAAWU,iBAGpBC,WAAWC,cAAcC,KAAKC,UAAUjB,SAAS,GAAMkB,MAAMC,sBAAaC,WAChFzB,eAAe0B,cAgBnBC,CAAgBpC"}
@@ -0,0 +1,3 @@
define("qbank_columnsortorder/repository",["exports","core/ajax"],(function(_exports,_ajax){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.setHiddenColumns=_exports.setColumnbankOrder=_exports.setColumnSize=_exports.resetColumns=void 0;_exports.setHiddenColumns=function(columns){let global=arguments.length>1&&void 0!==arguments[1]&&arguments[1];return(0,_ajax.call)([{methodname:"qbank_columnsortorder_set_hidden_columns",args:{columns:columns,global:global}}])[0]};_exports.setColumnbankOrder=function(columns){let global=arguments.length>1&&void 0!==arguments[1]&&arguments[1];return(0,_ajax.call)([{methodname:"qbank_columnsortorder_set_columnbank_order",args:{columns:columns,global:global}}])[0]};_exports.setColumnSize=function(sizes){let global=arguments.length>1&&void 0!==arguments[1]&&arguments[1];return(0,_ajax.call)([{methodname:"qbank_columnsortorder_set_column_size",args:{sizes:sizes,global:global}}])[0]};_exports.resetColumns=function(){let global=arguments.length>0&&void 0!==arguments[0]&&arguments[0];return Promise.all((0,_ajax.call)([{methodname:"qbank_columnsortorder_set_column_size",args:{global:global}},{methodname:"qbank_columnsortorder_set_columnbank_order",args:{global:global}},{methodname:"qbank_columnsortorder_set_hidden_columns",args:{global:global}}]))}}));
//# sourceMappingURL=repository.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"repository.min.js","sources":["../src/repository.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 * External function calls for qbank_columnsortorder\n *\n * @module qbank_columnsortorder/repository\n * @copyright 2023 Catalyst IT Europe Ltd.\n * @author Mark Johnson <mark.johnson@catalyst-eu.net>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {call as fetchMany} from 'core/ajax';\n\n/**\n * Save the list of hidden columns\n *\n * @param {String[]} columns List of hidden column names\n * @param {Boolean} global Set global config setting, rather than user preference\n * @return {Promise}\n */\nexport const setHiddenColumns = (columns, global = false) => fetchMany([{\n methodname: 'qbank_columnsortorder_set_hidden_columns',\n args: {\n columns,\n global,\n },\n}])[0];\n\n/**\n * Save the order of columns\n *\n * @param {String[]} columns List of column names in the desired order\n * @param {Boolean} global Set global config setting, rather than user preference\n * @return {Promise}\n */\nexport const setColumnbankOrder = (columns, global = false) => fetchMany([{\n methodname: 'qbank_columnsortorder_set_columnbank_order',\n args: {\n columns,\n global,\n },\n}])[0];\n\n/**\n * Save the column widths\n *\n * @param {String} sizes JSON string encoding an array of objects with \"column\" and \"width\" properties.\n * @param {Boolean} global Set global config setting, rather than user preference\n * @return {Promise}\n */\nexport const setColumnSize = (sizes, global = false) => fetchMany([{\n methodname: 'qbank_columnsortorder_set_column_size',\n args: {\n sizes,\n global,\n },\n}])[0];\n\n/**\n * Reset all settings.\n *\n * @param {Boolean} global Reset global config settings, rather than user preference\n * @return {Promise}\n */\nexport const resetColumns = (global = false) => Promise.all(\n fetchMany([\n {\n methodname: 'qbank_columnsortorder_set_column_size',\n args: {\n global,\n },\n },\n {\n methodname: 'qbank_columnsortorder_set_columnbank_order',\n args: {\n global,\n },\n },\n {\n methodname: 'qbank_columnsortorder_set_hidden_columns',\n args: {\n global,\n },\n },\n ])\n);\n"],"names":["columns","global","methodname","args","sizes","Promise","all"],"mappings":"wRAiCgC,SAACA,aAASC,sEAAmB,cAAU,CAAC,CACpEC,WAAY,2CACZC,KAAM,CACFH,QAAAA,QACAC,OAAAA,WAEJ,gCAS8B,SAACD,aAASC,sEAAmB,cAAU,CAAC,CACtEC,WAAY,6CACZC,KAAM,CACFH,QAAAA,QACAC,OAAAA,WAEJ,2BASyB,SAACG,WAAOH,sEAAmB,cAAU,CAAC,CAC/DC,WAAY,wCACZC,KAAM,CACFC,MAAAA,MACAH,OAAAA,WAEJ,0BAQwB,eAACA,sEAAmBI,QAAQC,KACpD,cAAU,CACN,CACIJ,WAAY,wCACZC,KAAM,CACFF,OAAAA,SAGR,CACIC,WAAY,6CACZC,KAAM,CACFF,OAAAA,SAGR,CACIC,WAAY,2CACZC,KAAM,CACFF,OAAAA"}
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,125 @@
// 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/>.
/**
* Common javascript for handling actions on the admin page and the user's view of the question bank.
*
* @module qbank_columnsortorder/actions
* @copyright 2023 onwards Catalyst IT Europe Ltd
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import SortableList from 'core/sortable_list';
import $ from 'jquery';
import * as repository from 'qbank_columnsortorder/repository';
import Notification from "core/notification";
import RefreshUi from 'core_question/refresh_ui';
export const SELECTORS = {
columnList: '.qbank-column-list',
sortableColumn: '.qbank-sortable-column',
removeLink: '[data-action=remove]',
moveHandler: '[data-drag-type=move]',
addColumn: '.addcolumn',
addLink: '[data-action=add]',
actionLink: '.action-link',
};
/**
* Sets up sortable list in the column sort order page.
*
* @param {Element} listRoot Element containing the sortable list.
* @param {Boolean} vertical Is the list in vertical orientation, rather than horizonal?
* @param {Boolean} global Should changes be saved to global config, rather than user preferences?
* @return {jQuery} sortable column elements, for attaching additional event listeners.
*/
export const setupSortableLists = (listRoot, vertical = false, global = false) => {
const sortableList = new SortableList(listRoot, {
moveHandlerSelector: SELECTORS.moveHandler,
isHorizontal: !vertical,
});
sortableList.getElementName = element => Promise.resolve(element.data('name'));
const sortableColumns = $(SELECTORS.sortableColumn);
sortableColumns.on(SortableList.EVENTS.DROP, () => {
repository.setColumnbankOrder(getColumnOrder(listRoot), global).catch(Notification.exception);
listRoot.querySelectorAll(SELECTORS.sortableColumn).forEach(item => item.classList.remove('active'));
});
sortableColumns.on(SortableList.EVENTS.DRAGSTART, (event) => {
event.currentTarget.classList.add('active');
});
return sortableColumns;
};
/**
* Set up event handlers for action buttons.
*
* For each action, call the web service to update the appropriate setting or user preference, then call the fragment to
* refresh the view.
*
* @param {Element} uiRoot The root of the question bank UI.
* @param {Boolean} global Should changes be saved to global config, rather than user preferences?
*/
export const setupActionButtons = (uiRoot, global = false) => {
uiRoot.addEventListener('click', async(e) => {
const actionLink = e.target.closest(SELECTORS.actionLink);
if (!actionLink) {
return;
}
try {
e.preventDefault();
const action = actionLink.dataset.action;
if (action === 'add' || action === 'remove') {
const hiddenColumns = [];
const addColumnList = document.querySelector(SELECTORS.addColumn);
if (addColumnList) {
addColumnList.querySelectorAll(SELECTORS.addLink).forEach(item => {
if (action === 'add' && item === actionLink) {
return;
}
hiddenColumns.push(item.dataset.column);
});
}
if (action === 'remove') {
hiddenColumns.push(actionLink.dataset.column);
}
await repository.setHiddenColumns(hiddenColumns, global);
} else if (action === 'reset') {
await repository.resetColumns(global);
}
const actionUrl = new URL(actionLink.href);
const returnUrl = new URL(actionUrl.searchParams.get('returnurl').replaceAll('&amp;', '&'));
await RefreshUi.refresh(uiRoot, returnUrl);
} catch (ex) {
await Notification.exception(ex);
}
});
};
/**
* Gets the newly reordered columns to display in the question bank view.
* @param {Element} listRoot
* @returns {Array}
*/
export const getColumnOrder = listRoot => {
const columns = Array.from(listRoot.querySelectorAll('[data-columnid]'))
.map(column => column.dataset.columnid);
return columns.filter((value, index) => columns.indexOf(value) === index);
};
@@ -0,0 +1,70 @@
// 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/>.
/**
* Javascript for handling actions on the admin page
*
* @module qbank_columnsortorder/admin_actions
* @copyright 2023 onwards Catalyst IT Europe Ltd
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import * as actions from 'qbank_columnsortorder/actions';
import * as repository from 'qbank_columnsortorder/repository';
import Notification from "core/notification";
import Pending from 'core/pending';
/**
* Event handler to save the custom column widths when a field is edited.
*
* @param {Element} listRoot The root element of the list of columns.
*/
const setupSaveWidths = listRoot => {
listRoot.addEventListener('change', async() => {
const pendingPromise = new Pending('saveWidths');
const columns = listRoot.querySelectorAll(actions.SELECTORS.sortableColumn);
const widths = [];
columns.forEach(column => {
const widthInput = column.querySelector('.width-input');
const valid = widthInput.checkValidity();
widthInput.closest('.has-validation').classList.add('was-validated');
if (!valid) {
return;
}
widths.push({
column: column.dataset.columnid,
width: widthInput.value,
});
});
await repository.setColumnSize(JSON.stringify(widths), true).catch(Notification.exception);
pendingPromise.resolve();
});
};
/**
* Initialize module
*
* Set up event handlers for the action buttons, width fields and initialise column sorting.
*
* @param {String} id ID for the admin UI root element.
*/
export const init = id => {
const uiRoot = document.getElementById(id);
const listRoot = uiRoot.querySelector(actions.SELECTORS.columnList);
actions.setupSortableLists(listRoot, true, true);
actions.setupActionButtons(uiRoot, true);
setupSaveWidths(listRoot);
};
@@ -0,0 +1,99 @@
// 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/>.
/**
* External function calls for qbank_columnsortorder
*
* @module qbank_columnsortorder/repository
* @copyright 2023 Catalyst IT Europe Ltd.
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import {call as fetchMany} from 'core/ajax';
/**
* Save the list of hidden columns
*
* @param {String[]} columns List of hidden column names
* @param {Boolean} global Set global config setting, rather than user preference
* @return {Promise}
*/
export const setHiddenColumns = (columns, global = false) => fetchMany([{
methodname: 'qbank_columnsortorder_set_hidden_columns',
args: {
columns,
global,
},
}])[0];
/**
* Save the order of columns
*
* @param {String[]} columns List of column names in the desired order
* @param {Boolean} global Set global config setting, rather than user preference
* @return {Promise}
*/
export const setColumnbankOrder = (columns, global = false) => fetchMany([{
methodname: 'qbank_columnsortorder_set_columnbank_order',
args: {
columns,
global,
},
}])[0];
/**
* Save the column widths
*
* @param {String} sizes JSON string encoding an array of objects with "column" and "width" properties.
* @param {Boolean} global Set global config setting, rather than user preference
* @return {Promise}
*/
export const setColumnSize = (sizes, global = false) => fetchMany([{
methodname: 'qbank_columnsortorder_set_column_size',
args: {
sizes,
global,
},
}])[0];
/**
* Reset all settings.
*
* @param {Boolean} global Reset global config settings, rather than user preference
* @return {Promise}
*/
export const resetColumns = (global = false) => Promise.all(
fetchMany([
{
methodname: 'qbank_columnsortorder_set_column_size',
args: {
global,
},
},
{
methodname: 'qbank_columnsortorder_set_columnbank_order',
args: {
global,
},
},
{
methodname: 'qbank_columnsortorder_set_hidden_columns',
args: {
global,
},
},
])
);
@@ -0,0 +1,397 @@
// 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/>.
/**
* Javascript for customising the user's view of the question bank
*
* @module qbank_columnsortorder/user_actions
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Ghaly Marc-Alexandre <marc-alexandreghaly@catalyst-ca.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import * as actions from 'qbank_columnsortorder/actions';
import * as repository from 'qbank_columnsortorder/repository';
import {get_string as getString} from 'core/str';
import ModalEvents from 'core/modal_events';
import ModalSaveCancel from 'core/modal_save_cancel';
import Notification from "core/notification";
import SortableList from 'core/sortable_list';
import Templates from "core/templates";
const SELECTORS = {
uiRoot: '.questionbankwindow',
moveAction: '.menu-action[data-action=move]',
resizeAction: '.menu-action[data-action=resize]',
resizeHandle: '.qbank_columnsortorder-action-handle.resize',
handleContainer: '.handle-container',
headerContainer: '.header-container',
tableColumn: identifier => `td[data-columnid="${identifier.replace(/["\\]/g, '\\$&')}"]`,
};
/** To track mouse event on a table header */
let currentHeader;
/** Current mouse x postion, to track mouse event on a table header */
let currentX;
/** Minimum size for the column currently being resized. */
let currentMin;
/**
* Flag to temporarily prevent move and resize handles from being shown or hidden.
*
* @type {boolean}
*/
let suspendShowHideHandles = false;
/**
* Add handle containers for move and resize handles.
*
* @param {Element} uiRoot The root element of the quesiton bank UI.
* @return {Promise} Resolved after the containers have been added to each column header.
*/
const addHandleContainers = uiRoot => {
return new Promise((resolve) => {
const headerContainers = uiRoot.querySelectorAll(SELECTORS.headerContainer);
Templates.renderForPromise('qbank_columnsortorder/handle_container', {})
.then(({html, js}) => {
headerContainers.forEach(container => {
Templates.prependNodeContents(container, html, js);
});
resolve();
return headerContainers;
}).catch(Notification.exception);
});
};
/**
* Render move handles in each container.
*
* This takes a list of the move actions rendered in each column header, and creates a corresponding drag handle for each.
*
* @param {NodeList} moveActions Menu actions for moving columns.
*/
const setUpMoveHandles = moveActions => {
moveActions.forEach(moveAction => {
const header = moveAction.closest('th');
header.classList.add('qbank-sortable-column');
const handleContainer = header.querySelector(SELECTORS.handleContainer);
const context = {
action: "move",
dragtype: "move",
target: '',
title: moveAction.title,
pixicon: "i/dragdrop",
pixcomponent: "core",
popup: true
};
return Templates.renderForPromise('qbank_columnsortorder/action_handle', context)
.then(({html, js}) => {
Templates.prependNodeContents(handleContainer, html, js);
return handleContainer;
}).catch(Notification.exception);
});
};
/**
* Serialise the current column sizes.
*
* This finds the current width set in each column header's style property, and returns them encoded as a JSON string.
*
* @param {Element} uiRoot The root element of the quesiton bank UI.
* @return {String} JSON array containing a list of objects with column and width properties.
*/
const serialiseColumnSizes = (uiRoot) => {
const columnSizes = [];
const tableHeaders = uiRoot.querySelectorAll('th');
tableHeaders.forEach(header => {
// Only get the width set via style attribute (set by move action).
const width = parseInt(header.style.width);
if (!width || isNaN(width)) {
return;
}
columnSizes.push({
column: header.dataset.columnid,
width: width
});
});
return JSON.stringify(columnSizes);
};
/**
* Find the minimum width for a header, based on the width of its contents.
*
* This is to simulate `min-width: min-content;`, which doesn't work on Chrome because
* min-width is ignored width `table-layout: fixed;`.
*
* @param {Element} header The table header
* @return {Number} The minimum width in pixels
*/
const getMinWidth = (header) => {
const contents = Array.from(header.querySelector('.header-text').children);
const contentWidth = contents.reduce((width, contentElement) => width + contentElement.getBoundingClientRect().width, 0);
return Math.ceil(contentWidth);
};
/**
* Render resize handles in each container.
*
* This takes a list of the resize actions rendered in each column header, and creates a corresponding drag handle for each.
* It also initialises the event handlers for the drag handles and resize modal.
*
* @param {Element} uiRoot Question bank UI root element.
*/
const setUpResizeHandles = (uiRoot) => {
const resizeActions = uiRoot.querySelectorAll(SELECTORS.resizeAction);
resizeActions.forEach(resizeAction => {
const headerContainer = resizeAction.closest(SELECTORS.headerContainer);
const header = resizeAction.closest(actions.SELECTORS.sortableColumn);
const minWidth = getMinWidth(header);
if (header.offsetWidth < minWidth) {
header.style.width = minWidth + 'px';
}
const handleContainer = headerContainer.querySelector(SELECTORS.handleContainer);
const context = {
action: "resize",
target: '',
title: resizeAction.title,
pixicon: 'i/twoway',
pixcomponent: 'core',
popup: true
};
return Templates.renderForPromise('qbank_columnsortorder/action_handle', context)
.then(({html, js}) => {
Templates.appendNodeContents(handleContainer, html, js);
return handleContainer;
}).catch(Notification.exception);
});
let moveTracker = false;
let currentResizeHandle = null;
// Start mouse event on headers.
uiRoot.addEventListener('mousedown', e => {
currentResizeHandle = e.target.closest(SELECTORS.resizeHandle);
// Return if it is not ' resize' button.
if (!currentResizeHandle) {
return;
}
// Save current position.
currentX = e.pageX;
// Find the header.
currentHeader = e.target.closest(actions.SELECTORS.sortableColumn);
currentMin = getMinWidth(currentHeader);
moveTracker = false;
suspendShowHideHandles = true;
});
// Resize column as the mouse move.
document.addEventListener('mousemove', e => {
if (!currentHeader || !currentResizeHandle || currentX === 0) {
return;
}
// Prevent text selection as the handle is dragged.
document.getSelection().removeAllRanges();
// Adjust the column width according the amount the handle was dragged.
const offset = e.pageX - currentX;
currentX = e.pageX;
const newWidth = currentHeader.offsetWidth + offset;
if (newWidth >= currentMin) {
currentHeader.style.width = newWidth + 'px';
}
moveTracker = true;
});
// Set new size when mouse is up.
document.addEventListener('mouseup', () => {
if (!currentHeader || !currentResizeHandle || currentX === 0) {
return;
}
if (moveTracker) {
// If the mouse moved, we are changing the size by drag, so save the change.
repository.setColumnSize(serialiseColumnSizes(uiRoot)).catch(Notification.exception);
} else {
// If the mouse didn't move, display a modal to change the size using a form.
showResizeModal(currentHeader, uiRoot);
}
currentMin = null;
currentHeader = null;
currentResizeHandle = null;
currentX = 0;
moveTracker = false;
suspendShowHideHandles = false;
});
};
/**
* Event handler for resize actions in each column header.
*
* This will listen for a click on any resize action, and activate the corresponding resize modal.
*
* @param {Element} uiRoot Question bank UI root element.
*/
const setUpResizeActions = uiRoot => {
uiRoot.addEventListener('click', (e) => {
const resizeAction = e.target.closest(SELECTORS.resizeAction);
if (resizeAction) {
e.preventDefault();
const currentHeader = resizeAction.closest('th');
showResizeModal(currentHeader, uiRoot);
}
});
};
/**
* Show a modal containing a number input for changing a column width without click-and-drag.
*
* @param {Element} currentHeader The header element that is being resized.
* @param {Element} uiRoot The question bank UI root element.
* @returns {Promise<void>}
*/
const showResizeModal = async(currentHeader, uiRoot) => {
const initialWidth = currentHeader.offsetWidth;
const minWidth = getMinWidth(currentHeader);
const modal = await ModalSaveCancel.create({
title: getString('resizecolumn', 'qbank_columnsortorder', currentHeader.dataset.name),
body: Templates.render('qbank_columnsortorder/resize_modal', {width: initialWidth, min: minWidth}),
show: true,
});
const root = modal.getRoot();
root.on(ModalEvents.cancel, () => {
currentHeader.style.width = `${initialWidth}px`;
});
root.on(ModalEvents.save, () => {
repository.setColumnSize(serialiseColumnSizes(uiRoot)).catch(Notification.exception);
});
const body = await modal.bodyPromise;
const input = body.get(0).querySelector('input');
input.addEventListener('change', e => {
const valid = e.target.checkValidity();
e.target.closest('.has-validation').classList.add('was-validated');
if (valid) {
const newWidth = e.target.value;
currentHeader.style.width = `${newWidth}px`;
}
});
};
/**
* Event handler for move actions in each column header.
*
* This will listen for a click on any move action, pass the click to the corresponding move handle, causing its modal to be shown.
*
* @param {Element} uiRoot Question bank UI root element.
*/
const setUpMoveActions = uiRoot => {
uiRoot.addEventListener('click', e => {
const moveAction = e.target.closest(SELECTORS.moveAction);
if (moveAction) {
e.preventDefault();
const sortableColumn = moveAction.closest(actions.SELECTORS.sortableColumn);
const moveHandle = sortableColumn.querySelector(actions.SELECTORS.moveHandler);
moveHandle.click();
}
});
};
/**
* Event handler for showing and hiding handles when the mouse is over a column header.
*
* Implementing this behaviour using the :hover CSS pseudoclass is not sufficient, as the mouse may move over the neighbouring
* header while dragging, leading to some odd behaviour. This allows us to suspend the show/hide behaviour while a handle is being
* dragged, and so keep the active handle visible until the drag is finished.
*
* @param {Element} uiRoot Question bank UI root element.
*/
const setupShowHideHandles = uiRoot => {
let shownHeader = null;
let tableHead = uiRoot.querySelector('thead');
uiRoot.addEventListener('mouseover', e => {
if (suspendShowHideHandles) {
return;
}
const header = e.target.closest(actions.SELECTORS.sortableColumn);
if (!header && !shownHeader) {
return;
}
if (!header || header !== shownHeader) {
tableHead.querySelector('.show-handles')?.classList.remove('show-handles');
shownHeader = header;
if (header) {
header.classList.add('show-handles');
}
}
});
};
/**
* Event handler for sortable list DROP event.
*
* Find all table cells corresponding to the column of the dropped header, and move them to the new position.
*
* @param {Event} event
*/
const reorderColumns = event => {
// Current header.
const header = event.target;
// Find the previous sibling of the header, which will be used when moving columns.
const insertAfter = header.previousElementSibling;
// Move columns.
const uiRoot = document.querySelector(SELECTORS.uiRoot);
const columns = uiRoot.querySelectorAll(SELECTORS.tableColumn(header.dataset.columnid));
columns.forEach(column => {
const row = column.parentElement;
if (insertAfter) {
// Find the column to insert after.
const insertAfterColumn = row.querySelector(SELECTORS.tableColumn(insertAfter.dataset.columnid));
// Insert the column.
insertAfterColumn.after(column);
} else {
// Insert as the first child (first column in the table).
row.insertBefore(column, row.firstChild);
}
});
};
/**
* Initialize module
*
* Add containers for the drag handles to each column header, then render handles, enable show/hide behaviour, set up drag/drop
* column sorting, then enable the move and resize modals to be triggered from menu actions.
*/
export const init = async() => {
const uiRoot = document.getElementById('questionscontainer');
await addHandleContainers(uiRoot);
setUpMoveHandles(uiRoot.querySelectorAll(SELECTORS.moveAction));
setUpResizeHandles(uiRoot);
setupShowHideHandles(uiRoot);
const sortableColumns = actions.setupSortableLists(uiRoot.querySelector(actions.SELECTORS.columnList));
sortableColumns.on(SortableList.EVENTS.DROP, reorderColumns);
sortableColumns.on(SortableList.EVENTS.DRAGSTART, () => {
suspendShowHideHandles = true;
});
sortableColumns.on(SortableList.EVENTS.DRAGEND, () => {
suspendShowHideHandles = false;
});
setUpMoveActions(uiRoot);
setUpResizeActions(uiRoot);
actions.setupActionButtons(uiRoot);
};
@@ -0,0 +1,412 @@
<?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_columnsortorder;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . '/questionlib.php');
use context_system;
use core_question\local\bank\column_base;
use core_question\local\bank\column_manager_base;
use core_question\local\bank\question_edit_contexts;
use core_question\local\bank\view;
use qbank_columnsortorder\local\bank\column_action_move;
use qbank_columnsortorder\local\bank\column_action_remove;
use qbank_columnsortorder\local\bank\column_action_resize;
use qbank_columnsortorder\local\bank\preview_view;
use moodle_url;
/**
* Class column_manager responsible for loading and saving order to the config setting.
*
* @package qbank_columnsortorder
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Ghaly Marc-Alexandre <marc-alexandreghaly@catalyst-ca.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class column_manager extends column_manager_base {
/**
* @var array Column order as set in config_plugins 'class' => 'position', ie: question_type_column => 3.
*/
public $columnorder;
/**
* @var array hidden columns.
*/
public $hiddencolumns;
/**
* @var array columns with size.
*/
public $colsize;
/**
* @var array Disabled columns in config_plugins table.
*/
public $disabledcolumns;
/**
* Constructor for column_manager class.
*
* @param bool $globalsettings Only use the global default settings, ignoring user preferences?
*/
public function __construct(bool $globalsettings = false) {
$this->columnorder = $this->setup_property('enabledcol', $globalsettings);
if (empty($this->columnorder)) {
$this->columnorder = [
'core_question\local\bank\checkbox_column' . column_base::ID_SEPARATOR . 'checkbox_column',
'qbank_viewquestiontype\question_type_column' . column_base::ID_SEPARATOR . 'question_type_column',
'qbank_viewquestionname\question_name_idnumber_tags_column' . column_base::ID_SEPARATOR .
'question_name_idnumber_tags_column',
'core_question\local\bank\edit_menu_column' . column_base::ID_SEPARATOR . 'edit_menu_column',
'qbank_editquestion\question_status_column' . column_base::ID_SEPARATOR . 'question_status_column',
'qbank_history\version_number_column' . column_base::ID_SEPARATOR . 'version_number_column',
'qbank_viewcreator\creator_name_column' . column_base::ID_SEPARATOR . 'creator_name_column',
'qbank_comment\comment_count_column' . column_base::ID_SEPARATOR . 'comment_count_column',
];
}
$this->hiddencolumns = $this->setup_property('hiddencols', $globalsettings);
$this->colsize = $this->setup_property('colsize', $globalsettings, 'json');
$this->disabledcolumns = $this->setup_property('disabledcol', true); // No user preference for disabledcol.
if ($this->columnorder) {
$this->columnorder = array_flip($this->columnorder);
}
if ($this->disabledcolumns) {
$this->disabledcolumns = array_flip($this->disabledcolumns);
}
}
/**
* Return the value for the given property, based the saved user preference or config setting.
*
* If no value is currently stored, returns an empty array.
*
* @param string $setting The identifier used for the saved config and user preference settings.
* @param bool $global Only get the global default, ignoring the user preference?
* @param string $encoding The encoding used to store the property - csv or json
* @return array
*/
private function setup_property(string $setting, bool $global = false, $encoding = 'csv'): array {
$value = get_config('qbank_columnsortorder', $setting);
if (!$global) {
$value = get_user_preferences("qbank_columnsortorder_{$setting}", $value);
}
if (empty($value)) {
return [];
}
return $encoding == 'csv' ? explode(',', $value) : json_decode($value);
}
/**
* Sets column order in the qbank_columnsortorder plugin config.
*
* @param ?array $columns Column order to set. Null value clears the setting.
* @param bool $global save this as a global default, rather than a user preference?
*/
public static function set_column_order(?array $columns, bool $global = false): void {
if (!is_null($columns)) {
$columns = implode(',', $columns);
}
self::save_preference('enabledcol', $columns, $global);
}
/**
* Hidden Columns.
*
* @param ?array $columns List of hidden columns. Null value clears the setting.
* @param bool $global save this as a global default, rather than a user preference?
*/
public static function set_hidden_columns(?array $columns, bool $global = false): void {
if (!is_null($columns)) {
$columns = implode(',', $columns);
}
self::save_preference('hiddencols', $columns, $global);
}
/**
* Column size.
*
* @param ?string $sizes columns with width. Null value clears the setting.
* @param bool $global save this as a global default, rather than a user preference?
*/
public static function set_column_size(?string $sizes, bool $global = false): void {
self::save_preference('colsize', $sizes, $global);
}
/**
* Save Preferences.
*
* @param string $name name of a configuration
* @param ?string $value value of a configuration. Null value clears the setting.
* @param bool $global save this as a global default, rather than a user preference?
*/
private static function save_preference(string $name, ?string $value, bool $global = false): void {
if ($global) {
require_capability('moodle/site:config', context_system::instance());
set_config($name, $value, 'qbank_columnsortorder');
} else {
set_user_preference("qbank_columnsortorder_{$name}", $value);
}
}
/**
* Get qbank.
*
* @return view
*/
public function get_questionbank(): view {
$course = (object) ['id' => 0];
$context = context_system::instance();
$contexts = new question_edit_contexts($context);
$category = question_make_default_categories($contexts->all());
$params = ['cat' => $category->id . ',' . $context->id];
// Dummy call to get the objects without error.
$questionbank = new preview_view(
$contexts,
new moodle_url('/question/bank/columnsortorder/sortcolumns.php'),
$course,
null,
$params
);
return $questionbank;
}
/**
* Get enabled columns.
*
* @return array
*/
public function get_columns(): array {
$columns = [];
foreach ($this->get_questionbank()->get_visiblecolumns() as $key => $column) {
if ($column->get_name() === 'checkbox') {
continue;
}
$columns[] = (object) [
'class' => get_class($column),
'name' => $column->get_title(),
'colname' => $column->get_column_name(),
'id' => $column->get_column_id(),
];
}
return $columns;
}
/**
* Get disabled columns.
*
* @return array
*/
public function get_disabled_columns(): array {
$result = $this->create_column_objects(array_keys($this->disabledcolumns));
$disabled = [];
foreach ($result as $column => $columnobject) {
$disabled[$column] = (object) [
'disabledname' => $columnobject->get_title(),
];
}
return $disabled;
}
/**
* Updates enabled and disabled config for 'qbank_columnsortorder' plugin.
*
* @param array $enabledcolumns Enabled columns to set.
* @param array $disabledcolumns Disabled columns to set.
*/
protected function update_config($enabledcolumns, $disabledcolumns): void {
if (!empty($enabledcolumns)) {
$configenabled = implode(',', array_flip($enabledcolumns));
set_config('enabledcol', $configenabled, 'qbank_columnsortorder');
}
if (!empty($disabledcolumns)) {
$configdisabled = implode(',', array_flip($disabledcolumns));
set_config('disabledcol', $configdisabled, 'qbank_columnsortorder');
} else {
set_config('disabledcol', null, 'qbank_columnsortorder');
}
}
/**
* Enables columns.
*
* @param string $plugin Plugin type and name ie: qbank_viewcreator.
*/
public function enable_columns(string $plugin): void {
$enabledcolumns = [];
$disabledcolumns = [];
if ($this->columnorder) {
$enabledcolumns = $this->columnorder;
}
if ($this->disabledcolumns) {
$disabledcolumns = $this->disabledcolumns;
foreach ($disabledcolumns as $class => $column) {
if (strpos($class, $plugin) !== false) {
$enabledcolumns[$class] = $class;
if (isset($disabledcolumns[$class])) {
unset($disabledcolumns[$class]);
}
}
}
}
$this->update_config($enabledcolumns, $disabledcolumns);
}
/**
* Disables columns.
*
* @param string $plugin Plugin type and name ie: qbank_viewcreator.
*/
public function disable_columns(string $plugin): void {
$disabledcolumns = [];
$enabledcolumns = [];
$allcolumns = $this->get_columns();
if ($this->disabledcolumns) {
$disabledcolumns = $this->disabledcolumns;
}
if ($this->columnorder) {
$enabledcolumns = $this->columnorder;
}
foreach ($allcolumns as $column) {
if (str_contains($column->class, $plugin)) {
$disabledcolumns[$column->id] = $column->id;
if (isset($enabledcolumns[$column->id])) {
unset($enabledcolumns[$column->id]);
}
}
}
$this->update_config($enabledcolumns, $disabledcolumns);
}
/**
* Orders columns in the question bank view according to config_plugins table 'qbank_columnsortorder' config.
*
* @param array $ordertosort Unordered array of columns, [columnname => class]
* @return array $properorder|$ordertosort Returns array ordered if 'qbank_columnsortorder' config exists.
*/
public function get_sorted_columns($ordertosort): array {
// Check if db has order set.
if (!empty($this->columnorder)) {
// Merge new order with old one.
$columnsortorder = $this->columnorder;
asort($columnsortorder);
$columnorder = [];
foreach ($columnsortorder as $columnid => $colposition) {
if (array_key_exists($columnid, $ordertosort)) {
$columnorder[$columnid] = $colposition;
}
}
$properorder = array_merge($columnorder, $ordertosort);
// Always have the checkbox at first column position.
$checkboxid = 'core_question\local\bank\checkbox_column' . column_base::ID_SEPARATOR . 'checkbox_column';
if (isset($properorder[$checkboxid])) {
$checkboxfirstelement = $properorder[$checkboxid];
unset($properorder[$checkboxid]);
$properorder = array_merge([
$checkboxid => $checkboxfirstelement
], $properorder);
}
return $properorder;
}
return $ordertosort;
}
/**
* Given an array of columns, set the isvisible attribute according to $this->hiddencolumns and $this->disabledcolumns.
*
* @param column_base[] $columns
* @return array
*/
public function set_columns_visibility(array $columns): array {
foreach ($columns as $column) {
if (!is_object($column)) {
continue;
}
$columnid = $column->get_column_id();
$column->isvisible = !in_array($columnid, $this->hiddencolumns) && !array_key_exists($columnid, $this->disabledcolumns);
}
return $columns;
}
/**
* Return $this->colsize mapped as an array of column name => width, excluding empty sizes.
*
* @return array
*/
public function get_colsize_map(): array {
$sizes = array_reduce($this->colsize, function($result, $colsize) {
$result[$colsize->column] = $colsize->width;
return $result;
}, []);
return array_filter($sizes);
}
/**
* Return an array of hidden columns as an array of class => column name
*
* @return array
*/
public function get_hidden_columns(): array {
$result = $this->create_column_objects($this->hiddencolumns);
$hidden = [];
foreach ($result as $column => $columnobject) {
$hidden[$column] = $columnobject->get_title();
}
return $hidden;
}
/**
* Returns an array of column objects.
*
* @param array $columnsnames Array of columns.
* @return column_base[] Array of $columnsname => $columnobject
*/
public function create_column_objects(array $columnsnames): array {
$result = [];
foreach ($columnsnames as $column) {
[$columnclass, $columnname] = explode(column_base::ID_SEPARATOR, $column, 2);
if (class_exists($columnclass)) {
$columnobject = $columnclass::from_column_name($this->get_questionbank(), $columnname, true);
if ($columnobject != null) {
$result[$column] = $columnobject;
}
}
}
return $result;
}
public function get_column_width(column_base $column): string {
$colsizemap = $this->get_colsize_map();
$columnid = $column->get_column_id();
if (array_key_exists($columnid, $colsizemap)) {
return $colsizemap[$columnid] . 'px';
}
return parent::get_column_width($column);
}
public function get_column_actions(view $qbank): array {
return [
new column_action_move($qbank),
new column_action_remove($qbank),
new column_action_resize($qbank),
];
}
}
@@ -0,0 +1,52 @@
<?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_columnsortorder\event;
use core\event\qbank_plugin_disabled;
use core\event\qbank_plugin_enabled;
use qbank_columnsortorder\column_manager;
/**
* Observer for qbank plugin enabled/disabled events
*
* @package qbank_columnsortorder
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class plugin_observer {
/**
* When a plugin is enabled, enable its columns.
*
* @param qbank_plugin_enabled $event
* @return void
*/
public static function plugin_enabled(qbank_plugin_enabled $event): void {
(new column_manager())->enable_columns($event->other['pluginname']);
}
/**
* When a plugin is disabled, disable its columns.
*
* @param qbank_plugin_disabled $event
* @return void
*/
public static function plugin_disabled(qbank_plugin_disabled $event): void {
(new column_manager())->disable_columns($event->other['pluginname']);
}
}
@@ -0,0 +1,89 @@
<?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_columnsortorder\external;
use context_system;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_value;
use qbank_columnsortorder\column_manager;
/**
* External function for saving column sizes.
*
* @package qbank_columnsortorder
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class set_column_size extends external_api {
/**
* Returns description of method parameters.
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters([
'sizes' => new external_value(
PARAM_TEXT,
'Size for each column, as a JSON string representing [column => size]',
VALUE_DEFAULT,
null,
),
'global' => new external_value(
PARAM_BOOL,
'Set global config setting, rather than user preference',
VALUE_DEFAULT,
false
),
]);
}
/**
* Returns description of method result value.
*/
public static function execute_returns(): void {
}
/**
* Set sizes for columns
* Save against user preference if component is specified
*
* @param ?string $sizes json string representing [column => size]. Null value clears the setting.
* @param bool $global Set global config setting, rather than user preference
*/
public static function execute(?string $sizes, bool $global = false): void {
[
'sizes' => $sizes,
'global' => $global,
] = self::validate_parameters(
self::execute_parameters(),
[
'sizes' => $sizes,
'global' => $global,
]
);
$context = context_system::instance();
self::validate_context($context);
if ($global) {
require_capability('moodle/site:config', $context);
}
column_manager::set_column_size($sizes, $global);
}
}
@@ -0,0 +1,85 @@
<?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_columnsortorder\external;
use context_system;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_multiple_structure;
use core_external\external_value;
use qbank_columnsortorder\column_manager;
/**
* External qbank_columnsortorder_set_columnbank_order API
*
* @package qbank_columnsortorder
* @category external
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author 2021, Ghaly Marc-Alexandre <marc-alexandreghaly@catalyst-ca.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class set_columnbank_order extends external_api {
/**
* Returns description of method parameters.
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters([
'columns' => new external_multiple_structure(
new external_value(PARAM_TEXT, 'Plugin name for the column', VALUE_REQUIRED),
'List of column in the desired order',
VALUE_DEFAULT,
null,
NULL_ALLOWED,
),
'global' => new external_value(PARAM_BOOL, 'Set global config setting, rather than user preference',
VALUE_DEFAULT, false),
]);
}
/**
* Returns description of method result value.
*
*/
public static function execute_returns(): void {
}
/**
* Set columns order.
*
* @param ?array $columns List of column names in the desired order. Null value clears the setting.
* @param bool $global Set global config setting, rather than user preference
*/
public static function execute(?array $columns, bool $global = false): void {
[
'columns' => $columns,
'global' => $global,
] = self::validate_parameters(self::execute_parameters(), [
'columns' => $columns,
'global' => $global,
]);
$context = context_system::instance();
self::validate_context($context);
if ($global) {
require_capability('moodle/site:config', $context);
}
column_manager::set_column_order($columns, $global);
}
}
@@ -0,0 +1,92 @@
<?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_columnsortorder\external;
use context_system;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_multiple_structure;
use core_external\external_value;
use qbank_columnsortorder\column_manager;
/**
* External function for saving the list of hidden columns.
*
* @package qbank_columnsortorder
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class set_hidden_columns extends external_api {
/**
* Returns description of method parameters.
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters([
'columns' => new external_multiple_structure(
new external_value(PARAM_TEXT, 'Plugin name for the hidden column', VALUE_REQUIRED),
'List of hidden columns',
VALUE_DEFAULT,
null,
NULL_ALLOWED,
),
'global' => new external_value(
PARAM_BOOL,
'Set global config setting, rather than user preference',
VALUE_DEFAULT,
false
),
]);
}
/**
* Returns description of method result value.
*
*/
public static function execute_returns(): void {
}
/**
* Set hidden columns
* Save against user preference if specified
*
* @param ?array $columns List of hidden columns. Null value clears the setting.
* @param bool $global Set global config setting, rather than user preference
*/
public static function execute(?array $columns, bool $global = false): void {
[
'columns' => $columns,
'global' => $global,
] = self::validate_parameters(
self::execute_parameters(),
[
'columns' => $columns,
'global' => $global,
]
);
$context = context_system::instance();
self::validate_context($context);
if ($global) {
require_capability('moodle/site:config', $context);
}
column_manager::set_hidden_columns($columns, $global);
}
}
@@ -0,0 +1,53 @@
<?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_columnsortorder\local\bank;
use core_question\local\bank\column_action_base;
use core_question\local\bank\column_base;
/**
* Move a column
*
* This will add an action menu item which will be enhanced by javascript in user_actions.js to show the move column modal for the
* current column when clicked.
*
* @package qbank_columnsortorder
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class column_action_move extends column_action_base {
/** @var string Label for the Move action. */
protected string $move;
protected function init(): void {
$this->move = get_string('move');
}
public function get_action_menu_link(column_base $column): ?\action_menu_link {
return new \action_menu_link_secondary(
new \moodle_url('/question/edit.php'),
new \pix_icon('i/dragdrop', ''),
$this->move,
[
'title' => get_string('movecolumn', 'qbank_columnsortorder', $column->get_title()),
'data-action' => 'move',
'data-column' => get_class($column),
]
);
}
}
@@ -0,0 +1,78 @@
<?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_columnsortorder\local\bank;
use core_question\local\bank\column_action_base;
use core_question\local\bank\column_base;
/**
* Remove a column
*
* This action will display a link that will set the current column as hidden, then redirect back the current page.
*
* @package qbank_columnsortorder
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class column_action_remove extends column_action_base {
/** @var bool True if we are changing global config, false for user preferences. */
protected bool $global;
/** @var string Label for the Remove action. */
protected string $remove;
protected function init(): void {
$this->global = false;
$this->remove = get_string('remove');
}
/**
* Set the $global property to indicate whether we are changing global config.
*
* This action is used on both the user and admin screens, so requires this additional method.
*
* @param bool $global
* @return void
*/
public function set_global(bool $global): void {
$this->global = $global;
}
public function get_action_menu_link(column_base $column): ?\action_menu_link {
$actionurl = new \moodle_url('/question/bank/columnsortorder/actions.php', [
'column' => $column->get_column_id(),
'action' => 'remove',
'sesskey' => sesskey(),
'returnurl' => new \moodle_url($this->qbank->returnurl),
]);
if ($this->global) {
$actionurl->param('global', $this->global);
}
return new \action_menu_link_secondary(
$actionurl,
new \pix_icon('t/delete', ''),
$this->remove,
[
'class' => 'action-link',
'title' => get_string('removecolumn', 'qbank_columnsortorder', $column->get_title()),
'data-action' => 'remove',
'data-column' => $column->get_column_id(),
]
);
}
}
@@ -0,0 +1,53 @@
<?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_columnsortorder\local\bank;
use core_question\local\bank\column_action_base;
use core_question\local\bank\column_base;
/**
* Resize a column
*
* This will add an action menu item which will be enhanced by javascript in user_actions.js to show the resize column modal for the
* current column when clicked.
*
* @package qbank_columnsortorder
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class column_action_resize extends column_action_base {
/** @var string Label for the resize action. */
protected string $resize;
protected function init(): void {
$this->resize = get_string('resize', 'qbank_columnsortorder');
}
public function get_action_menu_link(column_base $column): ?\action_menu_link {
return new \action_menu_link_secondary(
new \moodle_url('/question/edit.php'),
new \pix_icon('i/twoway', ''),
$this->resize,
[
'title' => get_string('resizecolumn', 'qbank_columnsortorder', $column->get_title()),
'data-action' => 'resize',
'data-column' => get_class($column),
]
);
}
}
@@ -0,0 +1,99 @@
<?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_columnsortorder\local\bank;
use core_question\local\bank\view;
use qbank_columnsortorder\column_manager;
/**
* Custom view for displaying a preview of the question bank
*
* @package qbank_columnsortorder
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class preview_view extends view {
/**
* Use global settings for the column manager.
*
* @return void
*/
protected function init_column_manager(): void {
$this->columnmanager = new column_manager(true);
}
/**
* Prints the table row with the preview data for each column.
*
* @param \stdClass $question
* @param int $rowcount
*/
public function print_table_row($question, $rowcount): void {
$rowclasses = implode(' ', $this->get_row_classes($question, $rowcount));
$attributes = [];
if ($rowclasses) {
$attributes['class'] = $rowclasses;
}
echo \html_writer::start_tag('tr', $attributes);
foreach ($this->visiblecolumns as $column) {
$column->display_preview($question, $rowclasses);
}
echo \html_writer::end_tag('tr');
foreach ($this->extrarows as $row) {
$row->display_preview($question, $rowclasses);
}
}
/**
* Get a dummy question containing valid data for the default question fields.
*
* @return \stdClass
*/
protected function get_dummy_question(): \stdClass {
return (object)[
'id' => 1,
'qtype' => 'truefalse',
'createdby' => 2,
'categoryid' => 1,
'contextid' => 1,
'status' => 'ready',
'version' => 1,
'versionid' => 1,
'questionbankentryid' => 1,
'name' => 'Lorem ipsum',
'idnumber' => 123,
'creatorfirstname' => 'Admin',
'creatorlastname' => 'User',
'timecreated' => 1691157311,
'modifierfirstname' => 'Admin',
'modifierlastname' => 'User',
'timemodified' => 1691157311,
];
}
/**
* Generate a preview of the question bank table with a single dummy question.
*
* @return string An HTML table containing the column headings and a single question row.
*/
public function get_preview(): string {
ob_start();
$this->display_questions([$this->get_dummy_question()]);
return ob_get_clean();
}
}
@@ -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_columnsortorder\output;
use core_reportbuilder\local\models\column;
use qbank_columnsortorder\column_manager;
use moodle_url;
use renderer_base;
/**
* Renderable for the "add column" dropdown list
*
* @package qbank_columnsortorder
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class add_column implements \renderable, \templatable {
/** @var column_manager The column manager for getting the list of hidden columns. */
protected column_manager $columnmanager;
/** @var moodle_url The current page URL to redirect back to. */
protected moodle_url $returnurl;
/** @var bool True if we are changing global config, false for user preferences. */
protected bool $global;
/**
* Store arguments for generating template context.
*
* @param column_manager $columnmanager
* @param moodle_url $returnurl
* @param bool $global
*/
public function __construct(column_manager $columnmanager, moodle_url $returnurl, bool $global = false) {
$this->columnmanager = $columnmanager;
$this->returnurl = $returnurl;
$this->global = $global;
}
public function export_for_template(renderer_base $output): array {
$hiddencolumns = [];
foreach ($this->columnmanager->get_hidden_columns() as $class => $name) {
$addurl = new moodle_url('/question/bank/columnsortorder/actions.php', [
'action' => 'add',
'global' => $this->global,
'column' => $class,
'sesskey' => sesskey(),
'returnurl' => $this->returnurl,
]);
$hiddencolumns[] = [
'name' => $name,
'addurl' => $addurl->out(false),
'column' => $class,
'addtext' => get_string('addcolumn', 'qbank_columnsortorder', $name),
];
}
return [
'hashiddencolumns' => !empty($hiddencolumns),
'hiddencolumns' => $hiddencolumns,
];
}
}
@@ -0,0 +1,54 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace qbank_columnsortorder\output;
use moodle_url;
use templatable;
use renderable;
use qbank_columnsortorder\column_manager;
/**
* Renderable for the question bank preview.
*
* This takes the HTML for a question bank preview, and displays in a page with a link to return to the admin screen.
*
* @package qbank_columnsortorder
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class column_sort_preview implements renderable, templatable {
/** @var string Rendered preview HTML. */
protected string $preview;
/**
* Store rendered preview for template context.
*
* @param string $preview
*/
public function __construct(string $preview) {
$this->preview = $preview;
}
public function export_for_template(\renderer_base $output): array {
$context = [
'backurl' => new moodle_url('/question/bank/columnsortorder/sortcolumns.php'),
'preview' => $this->preview,
];
return $context;
}
}
@@ -0,0 +1,99 @@
<?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_columnsortorder\output;
use core_question\local\bank\column_base;
use qbank_columnsortorder\local\bank\column_action_remove;
use moodle_url;
use qbank_columnsortorder\column_manager;
use renderable;
use templatable;
/**
* Renderable for the column sort admin UI.
*
* Displays a list of the currently enabled columns and allows them to be sorted, hidden, and resized.
*
* @package qbank_columnsortorder
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class column_sort_ui implements renderable, templatable {
/**
* The minimum custom width for a column.
*
* This is based on the minimum possible width of the smallest core column (question type).
* When viewed, the width will be resized to the minimum width of the column header, if too small.
*
* @var int
*/
const MIN_COLUMN_WIDTH = 30;
public function export_for_template(\renderer_base $output): array {
$columnmanager = new column_manager(true);
$enabledcolumns = $columnmanager->get_columns();
$disabledcolumns = $columnmanager->get_disabled_columns();
$columnsizes = $columnmanager->get_colsize_map();
$qbank = $columnmanager->get_questionbank();
$returnurl = new moodle_url('/question/bank/columnsortorder/sortcolumns.php');
$params = [];
$params['formaction'] = new moodle_url('/question/bank/columnsortorder/actions.php');
$params['sesskey'] = sesskey();
$params['disabled'] = $disabledcolumns;
$params['contextid'] = \context_system::instance()->id;
$params['minwidth'] = self::MIN_COLUMN_WIDTH;
foreach ($enabledcolumns as $column) {
if (in_array($column->id, $columnmanager->hiddencolumns) || array_key_exists($column->id, $disabledcolumns)) {
continue;
}
$name = $column->name;
$colname = get_string('qbankcolumnname', 'qbank_columnsortorder', $column->colname);
$removeaction = new column_action_remove($qbank);
$removeaction->set_global(true);
$actionmenu = new \action_menu([
$removeaction->get_action_menu_link($column->class::from_column_name($qbank, $column->colname)),
]);
$params['names'][] = [
'name' => $name,
'colname' => $colname,
'class' => $column->class,
'width' => $columnsizes[$column->id] ?? null,
'widthlabel' => get_string('width', 'qbank_columnsortorder', $name),
'actionmenu' => $actionmenu->export_for_template($output),
'columnid' => $column->id,
'escapedid' => str_replace('\\', '__', $column->id),
];
}
$params['disabled'] = array_values($disabledcolumns);
$params['columnsdisabled'] = !empty($params['disabled']);
$addcolumn = new add_column($columnmanager, $returnurl);
$params['addcolumn'] = $addcolumn->export_for_template($output);
$resetcolums = new reset_columns($returnurl);
$params['resetcolumns'] = $resetcolums->export_for_template($output);
$params['extraclasses'] = 'pr-1';
$urltoredirect = new moodle_url('/admin/settings.php', ['section' => 'manageqbanks']);
$params['urltomanageqbanks'] = get_string('qbankgotomanageqbanks', 'qbank_columnsortorder', $urltoredirect->out());
$params['previewurl'] = new moodle_url('/question/bank/columnsortorder/sortcolumns.php', [
'preview' => true,
]);
return $params;
}
}
@@ -0,0 +1,37 @@
<?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_columnsortorder\output;
/**
* Output fragments for the column sort order interface
*
* @package qbank_columnsortorder
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class fragment {
/**
* Render the column sort UI with the current global config.
*
* @return string
*/
public static function column_sort_ui(): string {
global $OUTPUT;
return $OUTPUT->render(new \qbank_columnsortorder\output\column_sort_ui());
}
}
@@ -0,0 +1,60 @@
<?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_columnsortorder\output;
use renderer_base;
/**
* Renderable for resetting customised column settings.
*
* This will display a link that resets all customised column settings and redirects back to the current page.
*
* @package qbank_columnsortorder
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class reset_columns implements \renderable, \templatable {
/** @var \moodle_url The current page URL to redirect back to. */
protected \moodle_url $returnurl;
/** @var bool True if we are changing global config, false for user preferences. */
protected bool $global;
/**
* Store data for generating the template context.
*
* @param \moodle_url $returnurl
* @param bool $global
*/
public function __construct(\moodle_url $returnurl, bool $global = false) {
$this->returnurl = $returnurl;
$this->global = $global;
}
public function export_for_template(renderer_base $output): array {
$reseturl = new \moodle_url('/question/bank/columnsortorder/actions.php', [
'action' => 'reset',
'global' => $this->global,
'sesskey' => sesskey(),
'returnurl' => $this->returnurl->out(),
]);
return [
'reseturl' => $reseturl->out(false),
];
}
}
@@ -0,0 +1,64 @@
<?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_columnsortorder;
use core\context;
use core_question\local\bank\column_manager_base;
use core_question\local\bank\plugin_features_base;
use core_question\local\bank\view;
use qbank_columnsortorder\output\add_column;
use qbank_columnsortorder\output\reset_columns;
/**
* Plugin features for qbank_columnsortorder
*
* @package qbank_columnsortorder
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class plugin_feature extends plugin_features_base {
/**
* Override the default column manager.
*
* This will set the column order, size and visibility based on the global settings defined on the admin screen, or on the
* current user's preference if they have set one.
*
* @return ?column_manager_base
*/
public function get_column_manager(): ?column_manager_base {
return new column_manager();
}
/**
* Return add and reset column controls.
*
* @param view $qbank The question bank view.
* @param context $context The current context, for permission checks.
* @param int $categoryid The current question category ID.
* @return \renderable[]
*/
public function get_question_bank_controls(view $qbank, context $context, int $categoryid): array {
global $PAGE;
$PAGE->requires->js_call_amd('qbank_columnsortorder/user_actions', 'init');
$returnurl = new \moodle_url($qbank->returnurl);
return [
200 => new add_column(new column_manager(), $returnurl),
300 => new reset_columns($returnurl),
];
}
}
@@ -0,0 +1,58 @@
<?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_columnsortorder\privacy;
use core_privacy\local\metadata\collection;
use core_privacy\local\request\writer;
/**
* Privacy provider for columnsortorder.
*
* @package qbank_columnsortorder
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Ghaly Marc-Alexandre <marc-alexandreghaly@catalyst-ca.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements
// The forms subsystem does not store any data itself, it has no database tables.
\core_privacy\local\metadata\provider,
// The forms subsystem has user preferences.
\core_privacy\local\request\user_preference_provider {
public static function get_metadata(collection $collection): collection {
$collection->add_user_preference('enabledcol', 'privacy:metadata:preference:enabledcol');
$collection->add_user_preference('hiddencols', 'privacy:metadata:preference:hiddencols');
$collection->add_user_preference('colsize', 'privacy:metadata:preference:colsize');
return $collection;
}
public static function export_user_preferences(int $userid) {
$components = ['core_question', "qbank_history"];
foreach ($components as $component) {
$prefnames = ['enabledcol', 'hiddencols', 'colsize'];
foreach ($prefnames as $name) {
$preference = get_user_preferences("{$component}_{$name}", null, $userid);
if ($preference !== null) {
$desc = get_string('enabledcol', 'privacy:metadata:preference:enabledcol');
writer::export_user_preference('qbank_columnsortorder', "{$component}_{$name}", $preference, $desc);
}
}
}
}
}
@@ -0,0 +1,92 @@
<?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/>.
/**
* Class defining resuable tests methods for external functions
*
* @package qbank_columnsortorder
* @copyright 2023 Catalyst IT Europe Ltd.
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace qbank_columnsortorder\tests;
/**
* Class defining resuable tests methods for external functions
*/
abstract class external_function_testcase extends \advanced_testcase {
/**
* @var string Fully-qualified external function class to test.
*/
protected $testclass;
/**
* @var string The name of the setting used to store the data.
*/
protected $setting;
/**
* @var bool Whether the data is stored as a comma-separated list.
*/
protected $csv = true;
/**
* A function that returns the data to be passed to the external function.
*
* The data returned will depend on the testclass.
*
* @return mixed
*/
abstract protected function generate_test_data(): mixed;
/**
* Test that execute() method sets the correct config setting.
*/
public function test_execute(): void {
$this->resetAfterTest(true);
$this->setAdminUser();
$testdata = $this->generate_test_data();
$this->testclass::execute($testdata, true);
$currentconfig = get_config('qbank_columnsortorder', $this->setting);
if ($this->csv) {
$currentconfig = explode(',', $currentconfig);
}
$this->assertEqualsCanonicalizing($testdata, $currentconfig);
}
/**
* Test that execute() method sets user preference when a component is passed.
*/
public function test_execute_user(): void {
$this->resetAfterTest(true);
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
$testdata = $this->generate_test_data();
$this->testclass::execute($testdata);
$userpreference = get_user_preferences('qbank_columnsortorder_' . $this->setting);
if ($this->csv) {
$userpreference = explode(',', $userpreference);
}
$this->assertEqualsCanonicalizing($testdata, $userpreference);
}
}
@@ -0,0 +1,37 @@
<?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/>.
/**
* Event observer registration
*
* @package qbank_columnsortorder
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$observers = [
[
'eventname' => '\core\event\qbank_plugin_enabled',
'callback' => '\qbank_columnsortorder\event\plugin_observer::plugin_enabled',
],
[
'eventname' => '\core\event\qbank_plugin_disabled',
'callback' => '\qbank_columnsortorder\event\plugin_observer::plugin_disabled',
],
];
@@ -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/>.
/**
* qbank_columnsortorder external functions and service definitions.
*
* @package qbank_columnsortorder
* @category webservice
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author 2021, Ghaly Marc-Alexandre <marc-alexandreghaly@catalyst-ca.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$functions = [
'qbank_columnsortorder_set_columnbank_order' => [
'classname' => 'qbank_columnsortorder\external\set_columnbank_order',
'description' => 'Sets question columns order in database',
'type' => 'write',
'ajax' => true,
],
'qbank_columnsortorder_set_hidden_columns' => [
'classname' => 'qbank_columnsortorder\external\set_hidden_columns',
'description' => 'Hidden Columns',
'type' => 'write',
'ajax' => true,
],
'qbank_columnsortorder_set_column_size' => [
'classname' => 'qbank_columnsortorder\external\set_column_size',
'description' => 'Column size',
'type' => 'write',
'ajax' => true,
],
];
@@ -0,0 +1,81 @@
<?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/>.
/**
* Custom sort order upgrade script.
*
* @package qbank_columnsortorder
* @copyright 2024 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use core_question\local\bank\column_base;
/**
* Upgrade the plugin.
*
* @param int $oldversion the version of this plugin we are upgrading from.
* @return bool success/failure.
*/
function xmldb_qbank_columnsortorder_upgrade(int $oldversion): bool {
global $DB;
if ($oldversion < 2024042201) {
// Before Moodle 4.3, config_plugins settings for qbank_columnsortorder (disabledcol, enabledcol) had a value like
// qbank_statistics\columns\facility_index,qbank_statistics\columns\discriminative_efficiency, ...
// In Moodle 4.3, the values are stored as qbank_statistics\columns\discriminative_efficiency-discriminative_efficiency.
// So updating the old values to match the new format.
// Update the columns records for qbank_columnsortorder plugin.
$pluginconfigs = $DB->get_records('config_plugins', ['plugin' => 'qbank_columnsortorder'], 'name');
foreach ($pluginconfigs as $config) {
if ($config->name == 'version') {
continue;
}
$fields = explode(',', $config->value);
$updatedcols = [];
foreach ($fields as $columnclass) {
// Columns config that are already in the correct format, could be ignored.
if (str_contains($columnclass, column_base::ID_SEPARATOR)) {
continue;
}
$classbits = explode('\\', $columnclass);
$columnname = end($classbits);
// The custom fields are to be in the format e.g., qbank_customfields\custom_field_column-test.
if (str_contains($columnclass, 'custom_field_column')) {
array_pop($classbits);
}
$updatedcols[] = implode('\\', $classbits) . column_base::ID_SEPARATOR . $columnname;
}
$updatedconfig = implode(',', $updatedcols);
set_config($config->name, $updatedconfig, 'qbank_columnsortorder');
}
// Custom sort order savepoint reached.
upgrade_plugin_savepoint(true, 2024042201, 'qbank', 'columnsortorder');
}
if ($oldversion < 2024042202) {
// Remove plugin entry created by previously incorrect 2024042201 savepoint.
$DB->delete_records('config_plugins', ['plugin' => 'qbank_qbank_columnsortorder']);
upgrade_plugin_savepoint(true, 2024042202, 'qbank', 'columnsortorder');
}
return true;
}
@@ -0,0 +1,47 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Strings for component qbank_columnsortorder, language 'en'.
*
* @package qbank_columnsortorder
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Ghaly Marc-Alexandre <marc-alexandreghaly@catalyst-ca.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['addcolumn'] = 'Add column \'{$a}\'';
$string['addcolumns'] = 'Add columns';
$string['auto'] = 'Auto';
$string['columnwidth'] = 'Column width (pixels)';
$string['invalidwidth'] = 'Width must be at least {$a}.';
$string['movecolumn'] = 'Move column \'{$a}\'';
$string['pluginname'] = 'Column sort order';
$string['privacy:metadata:preference:enabledcol'] = 'The Column sort order question bank plugin saves user preference of column orders.';
$string['privacy:metadata:preference:hiddencols'] = 'The Column sort order question bank plugin saves user preference of hidden columns.';
$string['privacy:metadata:preference:colsize'] = 'The Column sort order question bank plugin saves user preference of column sizes.';
$string['qbankcolumnsortorder'] = 'Column sort order';
$string['qbankgotocolumnsort'] = 'You can change the order of the columns in the question bank on the page {$a}.';
$string['qbankcolumnsdisabled'] = 'Currently disabled question bank plugins:';
$string['qbankgotomanageqbanks'] = 'You can remove a column by disabling the plugin in <a href=\'{$a}\'>Manage question bank plugins</a>.';
$string['qbankcolumnname'] = '{$a}';
$string['qbanksortdescription'] = 'The order in which plugins are listed below determines the order of the columns in the question bank.';
$string['resetcolumns'] = 'Reset columns';
$string['resize'] = 'Resize';
$string['resizecolumn'] = 'Resize column {$a}';
$string['removecolumn'] = 'Remove column \'{$a}\'';
$string['showhidecolumn'] = 'Show/hide column';
$string['width'] = 'Width of \'{$a}\' in pixels';
+36
View File
@@ -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/>.
/**
* Standard callback functions for qbank_columnsortorder
*
* This file only exists for defining fragment callbacks. Do not include any other functions here.
*
* @package qbank_columnsortorder
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Render the column sort UI. {@see \qbank_columnsortorder\output\fragment::column_sort_ui()}
*
* @param array $args
* @return string
*/
function qbank_columnsortorder_output_fragment_column_sort_ui(array $args): string {
return \qbank_columnsortorder\output\fragment::column_sort_ui();
}
@@ -0,0 +1,44 @@
<?php
// This file is part of Moodle - https://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/>.
/**
* Adds admin settings for the plugin.
*
* @package qbank_columnsortorder
* @category admin
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Ghaly Marc-Alexandre <marc-alexandreghaly@catalyst-ca.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
// Column sort order link in manageqbanks page.
$url = new moodle_url('/question/bank/columnsortorder/sortcolumns.php', ['section' => 'columnsortorder']);
if ($ADMIN->fulltree) {
$page = $adminroot->locate('manageqbanks');
if (isset($page)) {
$page->add(new admin_setting_description(
'manageqbanksgotocolumnsort',
'',
new lang_string('qbankgotocolumnsort', 'qbank_columnsortorder',
html_writer::link($url, get_string('qbankcolumnsortorder', 'qbank_columnsortorder')))
));
}
}
// Column sort order link in admin page.
$settings = new admin_externalpage('qbank_columnsortorder', get_string('qbankcolumnsortorder', 'qbank_columnsortorder'), $url);
@@ -0,0 +1,44 @@
<?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/>.
/**
* Question bank settings page class.
*
* @package qbank_columnsortorder
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Ghaly Marc-Alexandre <marc-alexandreghaly@catalyst-ca.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__ . '/../../../config.php');
require_once($CFG->libdir.'/adminlib.php');
admin_externalpage_setup('qbank_columnsortorder', '', ['section' => 'columnsortorder'],
'/question/bank/columnsortorder/sortcolumns.php');
$preview = optional_param('preview', false, PARAM_BOOL);
echo $OUTPUT->header();
echo $OUTPUT->heading(new lang_string('qbankcolumnsortorder', 'qbank_columnsortorder'));
if ($preview) {
$columnmanager = new \qbank_columnsortorder\column_manager(true);
$preview = $columnmanager->get_questionbank()->get_preview();
echo $OUTPUT->render(new \qbank_columnsortorder\output\column_sort_preview($preview));
} else {
echo $OUTPUT->render(new \qbank_columnsortorder\output\column_sort_ui());
}
echo $OUTPUT->footer();
+43
View File
@@ -0,0 +1,43 @@
#page-admin-question-bank-columnsortorder-sortcolumns .addcolumn {
display: inline-block;
}
.qbank-sortable-column {
background-color: white;
position: relative;
}
.jsenabled .qbank-column-list button.savewidths {
display: none;
}
.qbank-sortable-column .qbank_columnsortorder-action-handle {
display: none;
}
.qbank-sortable-column.show-handles .qbank_columnsortorder-action-handle {
display: block;
}
.qbank-sortable-column .handle-container {
pointer-events: none; /* Prevent the handle container blocking clicks to elements in the header */
width: 100%;
position: absolute;
top: 40%;
z-index: 1;
}
.qbank_columnsortorder-action-handle {
pointer-events: auto; /* Ensure the handles themselves receive clicks */
}
.qbank_columnsortorder-action-handle.move {
margin-left: -21px;
width: 16px;
}
.qbank_columnsortorder-action-handle.resize {
cursor: col-resize;
position: absolute;
top: 0;
right: 5px;
}
.qbank_columnsortorder-action-handle.resize img {
/* Prevent the resize handle icon being dragged across the page */
pointer-events: none;
}
.qbank_columnsortorder-action-handle .icon {
margin-right: 0;
}
@@ -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_columnsortorder/action_handle
Render a single action handle for customising a column.
Example context (json):
{
"action": "move",
"target": "",
"title": "Move column A",
"pixicon": "i/dragdrop",
"pixcomponent": "core",
"popup": true
}
}}
<span class="qbank_columnsortorder-action-handle {{action}}" tabindex="0" role="button"{{#popup}} aria-haspopup="true"{{/popup}}
data-action="{{action}}"
data-target="{{target}}"
{{#dragtype}}data-drag-type="{{dragtype}}" {{/dragtype}}
title="{{title}}">
{{#pixicon}}
{{#pix}}{{pixicon}}, {{pixcomponent}}{{/pix}}
{{/pixicon}}
{{#icon}}
<i class="fa fa-{{{icon}}} mr-1" aria-hidden="true"></i>
{{/icon}}
{{#text}}
{{text}}
{{/text}}
</span>
@@ -0,0 +1,57 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template qbank_columnsortorder/add_column
Add column widget
Displays a list of currently hidden columns, with links to add them back to the question bank.
Context variables required for this template:
* hiddencolumns - list of currently hidden columns
Example context (json):
{
"hashiddencolumns": true,
"hiddencolumns": [
{
"name": "Column A",
"class": "class_name_A",
"addurl": "/question/bank/columnsortorder/actions.php?action=add&column=class_name_A"
},
{
"name": "Column B",
"class": "class_name_B",
"addurl": "/question/bank/columnsortorder/actions.php?action=add&column=class_name_B"
}
]
}
}}
{{#hashiddencolumns}}
<div class="dropdown addcolumn">
<button class="btn btn-outline-dark dropdown-toggle ml-1" data-toggle="dropdown" id="addcolumndropdown" aria-haspopup="true" aria-expanded="false">
{{#str}}addcolumns, qbank_columnsortorder{{/str}}
</button>
<div class="dropdown-menu" aria-labelledby="addcolumndropdown">
{{#hiddencolumns}}
<a class="dropdown-item action-link" href="{{addurl}}" title="{{addtext}}" data-action="add" data-column="{{column}}">
{{name}}
</a>
{{/hiddencolumns}}
</div>
</div>
{{/hashiddencolumns}}
@@ -0,0 +1,35 @@
{{!
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_columnsortorder/column_sort_preview
Display a preview of the question bank, with a button to return to the customisation UI.
Context variables required for this template:
* backurl - URL of customisation UI page.
* preview - Rendered HTML for a question bank table with the current settings.
Example context (json):
{
"backurl": "https://example.com/question/bank/columnsortorder/sortcolumns.php",
"preview": "<table></table>"
}
}}
<div id="qbank_columnsortorder-{{uniqid}}" class="container">
<a class="btn btn-primary" href="{{backurl}}">{{#str}} back {{/str}}</a>
{{{preview}}}
</div>
@@ -0,0 +1,181 @@
{{!
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_columnsortorder/column_sort_ui
Display a list of enable columns with customisation controls, and a list of columns from disabled plugins, with a link to the
question bank plugin management screen.
Context variables required for this template:
* contextid - Current context id.
* formaction - The URL of the actions controller to submit the form to.
* sesskey - The current sesskey.
* previewurl - The URL of the question bank preview page.
* addcolumn - The context for the qbank_columnsortorder/add_column template.
* resetcolumns - The context for the qbank_columnsortorder/reset_columns template.
* names - A list of the currently active columns
* name - The display name of the column
* tiptitle - The tooltip text for the move handle.
* colname - The unique class name for the column from the plugin.
* escapedclass - The class name with \ replaced by __.
* widthlabel - The label text of for this column's width field.
* minwidth - The minimum value for the width field.
* width - The current value for the width field.
* actionmenu - The context for core/action_menu, a list of actions for the column.
* columnsdisabled - Are the any columns defined by disabled plugins?
* disabled - A list of disabled column names.
* disabledname - The disabled column's name.
* urltomanageqbanks - Link to the page for managing qbank plugins.
Example context (json):
{
"contextid": 1,
"formaction": "https://example.com/question/bank/columnsortorder/actions.php",
"sesskey": "12345abcde",
"previewurl": "https://example.com/question/bank/columnsortorder/sortcolumns.php?preview=1",
"addcolumn": {
"hashiddencolumns": true,
"hiddencolumns": [
{
"name": "Column A",
"class": "class_name_A",
"addurl": "/question/bank/columnsortorder/actions.php?action=add&column=class_name_A"
},
{
"name": "Column B",
"class": "class_name_B",
"addurl": "/question/bank/columnsortorder/actions.php?action=add&column=class_name_B"
}
]
},
"resetcolumns": {
"reseturl": "https://example.com/question/bank/columnsortorder/actions.php?action=reset"
},
"names": [
{
"name": "Column A",
"tiptitle": "Move Column A",
"columnid": "qbank_example\\col_name_A-col_name_A",
"escapedid": "qbank_example__col_name_A-col_name_A",
"widthlabel": "Width of Column A",
"minwidth": "10",
"width": ""
},
{
"name": "Column B",
"tiptitle": "Move Column B",
"columnid": "qbank_example\\col_name_B-col_name_B",
"escapedid": "qbank_example__col_name_B-col_name_B",
"minwidth": "10",
"width": "200"
}
],
"columnsdisabled": true,
"disabled": [
{
"disabledname": "disabled_1"
},
{
"disabledname": "disabled_2"
}
],
"urltomanageqbanks": "<a href=\"https://example.com/admin/manageqbankplugins.php\">Manage qbank plugins</a>"
}
}}
<div id="qbank_columnsortorder-{{uniqid}}" {{!
}}class="container" {{!
}}data-component="qbank_columnsortorder" {{!
}}data-callback="column_sort_ui" {{!
}}data-contextid="{{contextid}}">
<p>
{{#str}}qbanksortdescription, qbank_columnsortorder{{/str}}
</p>
<form class="has-validation" action="{{formaction}}" method="post">
<input type="hidden" name="sesskey" value="{{sesskey}}">{{!
}}<div class="d-grid gap-2 d-md-flex justify-content-between mb-2">
<div>
<a class="btn btn-primary" href="{{previewurl}}">
{{#str}}preview{{/str}}
</a>
{{#addcolumn}}
{{>qbank_columnsortorder/add_column}}
{{/addcolumn}}
</div>
{{#resetcolumns}}
{{>qbank_columnsortorder/reset_columns}}
{{/resetcolumns}}
</div>
<table class="generaltable table table-fixed">
<thead>
<tr>
<th scope="col"></th>
<th scope="col">{{#str}}name{{/str}}</th>
<th scope="col">{{#str}}plugin{{/str}}</th>
<th scope="col" class="w-25">{{#str}}columnwidth, qbank_columnsortorder{{/str}}</th>
<th scope="col">{{#str}}action{{/str}}</th>
</tr>
</thead>
<tbody class="qbank-column-list">
{{#names}}
<tr class="qbank-sortable-column" data-pluginname="{{class}}" data-name="{{name}}" data-columnid="{{columnid}}">
<td>{{>core/drag_handle}}</td>
<td>{{name}}</td>
<td>{{colname}}</td>
<td>
<div class="input-group">
<label class="sr-only" for="width_{{escapedid}}">{{widthlabel}}</label>
<input id="width_{{escapedid}}" {{!
}}class="form-control width-input" {{!
}}type="number" {{!
}}min="{{minwidth}}" {{!
}}name="width[{{escapedid}}]" {{!
}}placeholder="{{#str}}auto, qbank_columnsortorder{{/str}}" {{!
}}value="{{width}}">
<button class="btn btn-sm btn-outline-dark savewidths" type="submit" name="action" value="savewidths">
{{#str}}save{{/str}}
</button>
<div class="invalid-feedback">
{{#str}}invalidwidth, qbank_columnsortorder, {{minwidth}}{{/str}}
</div>
</div>
</td>
<td>{{#actionmenu}}{{>core/action_menu}}{{/actionmenu}}</td>
</tr>
{{/names}}
</tbody>
</table>
</form>
<div>
{{#columnsdisabled}}
{{#str}}qbankcolumnsdisabled, qbank_columnsortorder{{/str}}
<br>
{{/columnsdisabled}}
{{#disabled}}
<div class="list-group-item disabled">
{{disabledname}}
</div>
{{/disabled}}
</div>
<div>
{{{urltomanageqbanks}}}
</div>
{{#js}}
require(['qbank_columnsortorder/admin_actions'], function(AdminTable) {
AdminTable.init("qbank_columnsortorder-{{uniqid}}");
});
{{/js}}
</div>
@@ -0,0 +1,27 @@
{{!
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_columnsortorder/handle_container
Container for move/resize handles.
Example context (json):
{
}
}}
<div class="handle-container">
</div>
@@ -0,0 +1,33 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template qbank_columnsortorder/reset_columns
Button to reset question bank column settings to defaults.
Context variables required for this template:
Example context (json):
{
"reseturl": "https://example.com/question/bank/columnsortorder/actions.php?action=reset"
}
}}
<span>
<a href="{{reseturl}}" class="btn btn-outline-dark action-link ml-1" data-action="reset">
{{#str}}resetcolumns, qbank_columnsortorder{{/str}}
</a>
</span>
@@ -0,0 +1,33 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template qbank_columnsortorder/resize_modal
This template renders the content of the resize modal, a number input to set the column width in pixels.
Example context (json):
{
}
}}
<form class="has-validation">
<label for="columnwidth">{{#str}}columnwidth, qbank_columnsortorder{{/str}}</label>
<input class="form-control" id="columnwidth" type="number" value="{{width}}" min="{{min}}">
<div class="invalid-feedback">
{{#str}}invalidwidth, qbank_columnsortorder, {{min}}{{/str}}
</div>
</form>
@@ -0,0 +1,152 @@
@qbank @qbank_columnsortorder @javascript
Feature: Set default question bank column order and size
In order to set sensible defaults for the question bank interface
As an admin
I want to hide, reorder, and resize columns
Scenario: Admin can reorder question bank columns
Given I change the window size to "large"
And I log in as "admin"
When I navigate to "Plugins > Question bank plugins > Column sort order" in site administration
And I drag "Created by" "qbank_columnsortorder > column move handle" and I drop it in "T" "qbank_columnsortorder > column move handle"
Then "Created by" "table_row" should appear before "T" "table_row"
And I reload the page
And "Created by" "table_row" should appear before "T" "table_row"
And I follow "Preview"
And "Created by" "qbank_columnsortorder > column header" should appear before "T" "qbank_columnsortorder > column header"
Scenario: Disabling a question bank plugin removes its columns
Given I log in as "admin"
When I navigate to "Plugins > Question bank plugins > Column sort order" in site administration
And I should see "Created by"
And I click on "Manage question bank plugins" "link"
And I click on "Disable" "link" in the "View creator" "table_row"
And I click on "Column sort order" "link"
Then "Currently disabled question bank plugins:" "text" should appear before "Created by" "text"
And I click on "Manage question bank plugins" "link"
And I click on "Enable" "link" in the "View creator" "table_row"
And I click on "Column sort order" "link"
Then I should not see "Currently disabled question bank plugins:"
And I should see "Created by"
Scenario: Admin can hide a column in site administration page
Given I log in as "admin"
And I navigate to "Plugins > Question bank plugins > Column sort order" in site administration
And "Created by" "table_row" should exist
When I click on "Actions menu" "link" in the "Created by" "table_row"
And I choose "Remove" in the open action menu
Then "Created by" "table_row" should not exist
And I reload the page
And "Created by" "table_row" should not exist
And I follow "Preview"
And "Created by" "qbank_columnsortorder > column header" should not exist
Scenario: Admin can show a column in site administration page
Given the following config values are set as admin:
| config | value | plugin |
| hiddencols | qbank_viewcreator\creator_name_column-creator_name_column | qbank_columnsortorder |
And I log in as "admin"
And I navigate to "Plugins > Question bank plugins > Column sort order" in site administration
And "Created by" "table_row" should not exist
When I press "Add columns"
And I follow "Created by"
Then "Created by" "table_row" should exist
And I reload the page
And "Created by" "table_row" should exist
And I follow "Preview"
And "Created by" "qbank_columnsortorder > column header" should exist
Scenario: Admin can resize a column in site administration page
Given I log in as "admin"
And I navigate to "Plugins > Question bank plugins > Column sort order" in site administration
And the field "Width of 'Question' in pixels" matches value ""
When I set the field "Width of 'Question' in pixels" to "400"
And I reload the page
Then the field "Width of 'Question' in pixels" matches value "400"
And I follow "Preview"
And the "style" attribute of "Question" "qbank_columnsortorder > column header" should contain "width: 400px"
Scenario: Custom fields can be reordered, resized, hidden and shown
Given I change the window size to "large"
And I log in as "admin"
And I navigate to "Plugins > Question bank plugins > Question custom fields" in site administration
And I press "Add a new category"
And I click on "Add a new custom field" "link"
And I follow "Checkbox"
And I set the following fields to these values:
| Name | checkboxcustomcolumn |
| Short name | chckcust |
And I press "Save changes"
And I should see "checkboxcustomcolumn"
And I navigate to "Plugins > Question bank plugins > Column sort order" in site administration
And "checkboxcustomcolumn" "table_row" should appear after "Comments" "table_row"
When I drag "checkboxcustomcolumn" "qbank_columnsortorder > column move handle" and I drop it in "Comments" "qbank_columnsortorder > column move handle"
And I set the field "Width of 'checkboxcustomcolumn' in pixels" to "200"
And I follow "Preview"
And "checkboxcustomcolumn" "qbank_columnsortorder > column header" should appear before "Comments" "qbank_columnsortorder > column header"
And the "style" attribute of "checkboxcustomcolumn" "qbank_columnsortorder > column header" should contain "width: 200px"
And I follow "Back"
And "checkboxcustomcolumn" "table_row" should appear before "Comments" "table_row"
And the field "Width of 'checkboxcustomcolumn' in pixels" matches value "200"
And I click on "Actions menu" "link" in the "checkboxcustomcolumn" "table_row"
And I choose "Remove" in the open action menu
And "checkboxcustomcolumn" "table_row" should not exist
And I follow "Preview"
And "checkboxcustomcolumn" "qbank_columnsortorder > column header" should not exist
And I follow "Back"
And I press "Add columns"
And I follow "checkboxcustomcolumn"
And "checkboxcustomcolumn" "table_row" should exist
And I follow "Preview"
And "checkboxcustomcolumn" "qbank_columnsortorder > column header" should exist
Scenario: Resetting columns on the admin page clears global settings
Given the following config values are set as admin:
| config | value | plugin |
| hiddencols | qbank_usage\question_last_used_column-question_last_used_column | qbank_columnsortorder |
| enabledcol | qbank_comment\comment_count_column-comment_count_column,qbank_viewquestionname\question_name_idnumber_tags_column-question_name_idnumber_tags_column,core_question\local\bank\edit_menu_column-edit_menu_column,qbank_editquestion\question_status_column-question_status_column,qbank_history\version_number_column-version_number_column,qbank_statistics\columns\discrimination_index:discrimination_index,qbank_statistics\columns\facility_index:facility_index,qbank_statistics\columns\discriminative_efficiency:discriminative_efficiency,qbank_usage\question_usage_column-question_usage_column,qbank_usage\question_last_used_column-question_last_used_column,qbank_viewcreator\creator_name_column-creator_name_column,qbank_viewcreator\modifier_name_column-modifier_name_column,qbank_viewquestiontype\question_type_column-question_type_column | qbank_columnsortorder |
| colsize | [{"column":"qbank_comment\\comment_count_column-comment_count_column","width":"20"},{"column":"qbank_viewquestionname\\question_name_idnumber_tags_column-question_name_idnumber_tags_column","width":"300"},{"column":"qbank_editquestion\\question_status_column-question_status_column","width":"20"},{"column":"qbank_history\\version_number_column-version_number_column","width":"20"},{"column":"qbank_statistics\\columns\\discrimination_index:discrimination_index","width":"20"},{"column":"qbank_statistics\\columns\\facility_index:facility_index","width":"20"},{"column":"qbank_statistics\\columns\\discriminative_efficiency:discriminative_efficiency","width":"20"},{"column":"qbank_usage\\question_usage_column-question_usage_column","width":"20"},{"column":"qbank_viewcreator\\creator_name_column-creator_name_column","width":"200"},{"column":"qbank_viewcreator\\modifier_name_column-modifier_name_column","width":"200"},{"column":"qbank_viewquestiontype\\question_type_column-question_type_column","width":"20"},{"column":"core_question\\local\\bank\\edit_menu_column-edit_menu_column","width":"50"}] | qbank_columnsortorder |
And I change the window size to "large"
And I log in as "admin"
And I navigate to "Plugins > Question bank plugins > Column sort order" in site administration
And "Last used" "table_row" should not exist
And "Comments" "table_row" should appear before "Question" "table_row"
And the field "Width of 'Question' in pixels" matches value "300"
When I follow "Reset columns"
Then "Last used" "table_row" should exist
And "Question" "table_row" should appear before "Comments" "table_row"
And the field "Width of 'Question' in pixels" matches value ""
Scenario: Deleting a custom field which is removed from the Column sort order
Given the following "custom field categories" exist:
| name | component | area | itemid |
| Category for test | qbank_customfields | question | 0 |
And the following "custom fields" exist:
| name | category | type | shortname | configdata |
| Field 1 | Category for test | text | f1 | {"visibility":"2"} |
| Field 2 | Category for test | text | f2 | {"visibility":"2"} |
And I change the window size to "large"
When I log in as "admin"
And I navigate to "Plugins > Question bank plugins > Column sort order" in site administration
And "Field 1" "table_row" should exist
And "Field 2" "table_row" should exist
And I click on "Actions menu" "link" in the "Field 1" "table_row"
And I choose "Remove" in the open action menu
# Delete a question custom field.
And I navigate to "Plugins > Question bank plugins > Question custom fields" in site administration
And I click on "Delete" "link" in the "Field 1" "table_row"
And I click on "Yes" "button" in the "Confirm" "dialogue"
And I navigate to "Plugins > Question bank plugins > Column sort order" in site administration
Then I should see "Column sort order"
And "Field 2" "table_row" should exist
And I click on "Actions menu" "link" in the "Field 2" "table_row"
And I choose "Remove" in the open action menu
# Delete the question custom category.
And I navigate to "Plugins > Question bank plugins > Question custom fields" in site administration
And I click on "[data-role='deletecategory']" "css_element"
And I click on "Yes" "button" in the "Confirm" "dialogue"
And I should not see "Category for test" in the "#customfield_catlist" "css_element"
And I navigate to "Plugins > Question bank plugins > Column sort order" in site administration
And I should see "Column sort order"
@@ -0,0 +1,43 @@
<?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/>.
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
require_once(__DIR__ . '/../../../../../lib/behat/behat_base.php');
/**
* Steps definitions related with the drag and drop header.
* @package qbank_columnsortorder
* @category test
* @copyright 2022 Catalyst IT Australia Pty Ltd
* @author Nathan Nguyen <nathannguyen@catalyst-ca.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_qbank_columnsortorder extends behat_base {
public static function get_exact_named_selectors(): array {
return [
new behat_component_named_selector('column header', [
"//th[contains(@data-name, %locator%)]",
]),
new behat_component_named_selector('column move handle', [
"//*[self::th or self::tr][contains(@data-name, %locator%)]//span[contains(@data-drag-type, 'move')]",
]),
new behat_component_named_selector('column resize handle', [
"//th[contains(@data-name, %locator%)]//span[contains(@data-action, 'resize')]",
]),
];
}
}
@@ -0,0 +1,178 @@
@qbank @qbank_columnsortorder @javascript
Feature: Set question bank column order and size
In order customise my view of the question bank
As a teacher
I want to hide, reorder, and resize columns
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | format |
| Course 1 | C1 | weeks |
And the following "activity" exists:
| activity | quiz |
| course | C1 |
| name | Test quiz Q001 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "question category" exist:
| contextlevel | reference | name |
| Activity module | Test quiz Q001 | Question category 1 |
And the following "questions" exist:
| questioncategory | qtype | name | user | questiontext | idnumber |
| Question category 1 | essay | Test question to be seen | teacher1 | Write about whatever you want | idnumber1 |
And the following config values are set as admin:
| config | value | plugin |
| hiddencols | qbank_usage\question_last_used_column-question_last_used_column | qbank_columnsortorder |
| enabledcol | qbank_comment\comment_count_column-comment_count_column,qbank_viewquestionname\question_name_idnumber_tags_column-question_name_idnumber_tags_column,core_question\local\bank\edit_menu_column-edit_menu_column,qbank_editquestion\question_status_column-question_status_column,qbank_history\version_number_column-version_number_column,qbank_statistics\columns\discrimination_index:discrimination_index,qbank_statistics\columns\facility_index:facility_index,qbank_statistics\columns\discriminative_efficiency:discriminative_efficiency,qbank_usage\question_usage_column-question_usage_column,qbank_usage\question_last_used_column-question_last_used_column,qbank_viewcreator\creator_name_column-creator_name_column,qbank_viewcreator\modifier_name_column-modifier_name_column,qbank_viewquestiontype\question_type_column-question_type_column | qbank_columnsortorder |
| colsize | [{"column":"qbank_comment\\comment_count_column-comment_count_column","width":"20"},{"column":"qbank_viewquestionname\\question_name_idnumber_tags_column-question_name_idnumber_tags_column","width":"300"},{"column":"qbank_editquestion\\question_status_column-question_status_column","width":"20"},{"column":"qbank_history\\version_number_column-version_number_column","width":"20"},{"column":"qbank_statistics\\columns\\discrimination_index:discrimination_index","width":"20"},{"column":"qbank_statistics\\columns\\facility_index:facility_index","width":"20"},{"column":"qbank_statistics\\columns\\discriminative_efficiency:discriminative_efficiency","width":"20"},{"column":"qbank_usage\\question_usage_column-question_usage_column","width":"20"},{"column":"qbank_viewcreator\\creator_name_column-creator_name_column","width":"200"},{"column":"qbank_viewcreator\\modifier_name_column-modifier_name_column","width":"200"},{"column":"qbank_viewquestiontype\\question_type_column-question_type_column","width":"20"},{"column":"core_question\\local\\bank\\edit_menu_column-edit_menu_column","width":"50"}] | qbank_columnsortorder |
Scenario: The teacher sees the question bank with global settings initially
Given I am on the "Test quiz Q001" "mod_quiz > question bank" page logged in as "teacher1"
When I apply question bank filter "Category" with value "Question category 1"
Then I should see "Test question to be seen"
And "Last used" "qbank_columnsortorder > column header" should not exist
And "Comments" "qbank_columnsortorder > column header" should appear before "Question" "qbank_columnsortorder > column header"
And the "style" attribute of "Question" "qbank_columnsortorder > column header" should contain "width: 300px;"
Scenario: User preference takes precedence over global defaults
Given the following "user preferences" exist:
| user | preference | value |
| teacher1 | qbank_columnsortorder_hiddencols | qbank_comment\comment_count_column-comment_count_column |
| teacher1 | qbank_columnsortorder_enabledcol | qbank_viewquestionname\question_name_idnumber_tags_column-question_name_idnumber_tags_column,qbank_usage\question_last_used_column-question_last_used_column,core_question\local\bank\edit_menu_column-edit_menu_column,qbank_editquestion\question_status_column-question_status_column,qbank_history\version_number_column-version_number_column,qbank_statistics\columns\discrimination_index:discrimination_index,qbank_statistics\columns\facility_index:facility_index,qbank_statistics\columns\discriminative_efficiency:discriminative_efficiency,qbank_usage\question_usage_column-question_usage_column,qbank_usage\question_last_used_column-question_last_used_column,qbank_viewcreator\creator_name_column-creator_name_column,qbank_viewcreator\modifier_name_column-modifier_name_column,qbank_viewquestiontype\question_type_column-question_type_column |
| teacher1 | qbank_columnsortorder_colsize | [{"column":"qbank_comment\\comment_count_column-comment_count_column","width":"20"},{"column":"qbank_viewquestionname\\question_name_idnumber_tags_column-question_name_idnumber_tags_column","width":"400"},{"column":"qbank_editquestion\\question_status_column-question_status_column","width":"20"},{"column":"qbank_history\\version_number_column-version_number_column","width":"20"},{"column":"qbank_statistics\\columns\\discrimination_index:discrimination_index","width":"20"},{"column":"qbank_statistics\\columns\\facility_index:facility_index","width":"20"},{"column":"qbank_statistics\\columns\\discriminative_efficiency:discriminative_efficiency","width":"20"},{"column":"qbank_usage\\question_usage_column-question_usage_column","width":"20"},{"column":"qbank_viewcreator\\creator_name_column-creator_name_column","width":"200"},{"column":"qbank_viewcreator\\modifier_name_column-modifier_name_column","width":"200"},{"column":"qbank_viewquestiontype\\question_type_column-question_type_column","width":"20"},{"column":"core_question\\local\\bank\\edit_menu_column-edit_menu_column","width":"50"}] |
And I am on the "Test quiz Q001" "mod_quiz > question bank" page logged in as "teacher1"
When I apply question bank filter "Category" with value "Question category 1"
Then "Comments" "qbank_columnsortorder > column header" should not exist
And "Question" "qbank_columnsortorder > column header" should appear before "Last used" "qbank_columnsortorder > column header"
And the "style" attribute of "Question" "qbank_columnsortorder > column header" should contain "width: 400px;"
Scenario: Resetting user view goes back to global defaults
Given the following "user preferences" exist:
| user | preference | value |
| teacher1 | qbank_columnsortorder_hiddencols | |
| teacher1 | qbank_columnsortorder_enabledcol | qbank_viewquestionname\question_name_idnumber_tags_column-question_name_idnumber_tags_column,qbank_usage\question_last_used_column-question_last_used_column,core_question\local\bank\edit_menu_column-edit_menu_column,qbank_editquestion\question_status_column-question_status_column,qbank_history\version_number_column-version_number_column,qbank_statistics\columns\discrimination_index:discrimination_index,qbank_statistics\columns\facility_index:facility_index,qbank_statistics\columns\discriminative_efficiency:discriminative_efficiency,qbank_usage\question_usage_column-question_usage_column,qbank_usage\question_last_used_column-question_last_used_column,qbank_viewcreator\creator_name_column-creator_name_column,qbank_viewcreator\modifier_name_column-modifier_name_column,qbank_viewquestiontype\question_type_column-question_type_column |
| teacher1 | qbank_columnsortorder_colsize | [{"column":"qbank_comment\\comment_count_column-comment_count_column","width":"20"},{"column":"qbank_viewquestionname\\question_name_idnumber_tags_column-question_name_idnumber_tags_column","width":"400"},{"column":"qbank_editquestion\\question_status_column-question_status_column","width":"20"},{"column":"qbank_history\\version_number_column-version_number_column","width":"20"},{"column":"qbank_statistics\\columns\\discrimination_index:discrimination_index","width":"20"},{"column":"qbank_statistics\\columns\\facility_index:facility_index","width":"20"},{"column":"qbank_statistics\\columns\\discriminative_efficiency:discriminative_efficiency","width":"20"},{"column":"qbank_usage\\question_usage_column-question_usage_column","width":"20"},{"column":"qbank_viewcreator\\creator_name_column-creator_name_column","width":"200"},{"column":"qbank_viewcreator\\modifier_name_column-modifier_name_column","width":"200"},{"column":"qbank_viewquestiontype\\question_type_column-question_type_column","width":"20"},{"column":"core_question\\local\\bank\\edit_menu_column-edit_menu_column","width":"50"}] |
And I am on the "Test quiz Q001" "mod_quiz > question bank" page logged in as "teacher1"
And I apply question bank filter "Category" with value "Question category 1"
And "Last used" "qbank_columnsortorder > column header" should exist
And "Question" "qbank_columnsortorder > column header" should appear before "Last used" "qbank_columnsortorder > column header"
And the "style" attribute of "Question" "qbank_columnsortorder > column header" should contain "width: 400px;"
When I follow "Reset columns"
Then "Last used" "qbank_columnsortorder > column header" should not exist
And "Comments" "qbank_columnsortorder > column header" should appear before "Question" "qbank_columnsortorder > column header"
And the "style" attribute of "Question" "qbank_columnsortorder > column header" should contain "width: 300px;"
Scenario: User can remove a column in the question bank
Given I am on the "Test quiz Q001" "mod_quiz > question bank" page logged in as "teacher1"
And I apply question bank filter "Category" with value "Question category 1"
And "Comments" "qbank_columnsortorder > column header" should exist
And I click on "Actions menu" "link" in the "Comments" "qbank_columnsortorder > column header"
And I choose "Remove" in the open action menu
Then "Comments" "qbank_columnsortorder > column header" should not exist
And I reload the page
And "Comments" "qbank_columnsortorder > column header" should not exist
Scenario: User can add a column in the question bank
Given I am on the "Test quiz Q001" "mod_quiz > question bank" page logged in as "teacher1"
And I apply question bank filter "Category" with value "Question category 1"
And "Last used" "qbank_columnsortorder > column header" should not exist
When I press "Add columns"
And I follow "Last used"
Then "Last used" "qbank_columnsortorder > column header" should exist
And I reload the page
And "Last used" "qbank_columnsortorder > column header" should exist
Scenario: User can resize a column in the question bank using modal dialog
Given I am on the "Test quiz Q001" "mod_quiz > question bank" page logged in as "teacher1"
And I apply question bank filter "Category" with value "Question category 1"
And the "style" attribute of "Question" "qbank_columnsortorder > column header" should contain "width: 300px"
When I click on "Actions menu" "link" in the "Question" "qbank_columnsortorder > column header"
And I choose "Resize" in the open action menu
And I set the field "Column width (pixels)" to "400"
And I press "Save changes"
Then the "style" attribute of "Question" "qbank_columnsortorder > column header" should contain "width: 400px"
And I reload the page
And the "style" attribute of "Question" "qbank_columnsortorder > column header" should contain "width: 400px"
Scenario: User can resize a column in the question bank by dragging
Given I change the window size to "large"
And I am on the "Test quiz Q001" "mod_quiz > question bank" page logged in as "teacher1"
And I apply question bank filter "Category" with value "Question category 1"
And the "style" attribute of "Question" "qbank_columnsortorder > column header" should contain "width: 300px"
When I hover "Question" "qbank_columnsortorder > column header"
And I drag "Question" "qbank_columnsortorder > column resize handle" and I drop it in "Status" "qbank_columnsortorder > column header"
And the "style" attribute of "Question" "qbank_columnsortorder > column header" should not contain "width: 300px"
And I reload the page
And the "style" attribute of "Question" "qbank_columnsortorder > column header" should not contain "width: 300px"
Scenario: User can move a column in the question bank using modal dialog
Given I am on the "Test quiz Q001" "mod_quiz > question bank" page logged in as "teacher1"
And I apply question bank filter "Category" with value "Question category 1"
And "Comments" "qbank_columnsortorder > column header" should appear before "Question" "qbank_columnsortorder > column header"
When I click on "Actions menu" "link" in the "Comments" "qbank_columnsortorder > column header"
And I choose "Move" in the open action menu
And I follow "After \"Question\""
Then "Comments" "qbank_columnsortorder > column header" should appear after "Question" "qbank_columnsortorder > column header"
And I reload the page
And "Comments" "qbank_columnsortorder > column header" should appear after "Question" "qbank_columnsortorder > column header"
Scenario: User can move a column in the question bank by dragging
Given I change the window size to "large"
And I am on the "Test quiz Q001" "mod_quiz > question bank" page logged in as "teacher1"
And I apply question bank filter "Category" with value "Question category 1"
And "Comments" "qbank_columnsortorder > column header" should appear before "Question" "qbank_columnsortorder > column header"
When I hover "Comments" "qbank_columnsortorder > column header"
And I drag "Comments" "qbank_columnsortorder > column move handle" and I drop it in "Status" "qbank_columnsortorder > column header"
Then "Comments" "qbank_columnsortorder > column header" should appear after "Question" "qbank_columnsortorder > column header"
And I reload the page
And "Comments" "qbank_columnsortorder > column header" should appear after "Question" "qbank_columnsortorder > column header"
Scenario: Reordering with disabled columns
When I log in as "admin"
And I navigate to "Plugins > Question bank plugins > Manage question bank plugins" in site administration
And I click on "Disable" "link" in the "Question statistics" "table_row"
And I click on "Enable" "link" in the "Question statistics" "table_row"
And I click on "Disable" "link" in the "Question statistics" "table_row"
And I am on the "Course 1" "core_question > course question bank" page
Then I should see "Question bank"
And "Create a new question" "button" should exist
# Really, we are just checking the question bank displayed without errors.
Scenario: Deleting a custom field which a user had removed from his preferences
Given the following "custom field categories" exist:
| name | component | area | itemid |
| Category for test | qbank_customfields | question | 0 |
And the following "custom fields" exist:
| name | category | type | shortname | configdata |
| Field 1 | Category for test | text | f1 | {"visibility":"2"} |
| Field 2 | Category for test | text | f2 | {"visibility":"2"} |
And I am on the "Test quiz Q001" "mod_quiz > question bank" page logged in as "teacher1"
And I apply question bank filter "Category" with value "Question category 1"
And I click on "Actions menu" "link" in the "Field 1" "qbank_columnsortorder > column header"
And I choose "Remove" in the open action menu
And I click on "Actions menu" "link" in the "Field 2" "qbank_columnsortorder > column header"
And I choose "Remove" in the open action menu
And "Field 2" "qbank_columnsortorder > column header" should not exist
# Delete a question custom field.
And I log in as "admin"
And I navigate to "Plugins > Question bank plugins > Question custom fields" in site administration
And I click on "Delete" "link" in the "Field 1" "table_row"
And I click on "Yes" "button" in the "Confirm" "dialogue"
And I am on the "Test quiz Q001" "mod_quiz > question bank" page logged in as "teacher1"
And I apply question bank filter "Category" with value "Question category 1"
Then I should see "Question bank"
# Delete the question custom category.
And I log in as "admin"
And I navigate to "Plugins > Question bank plugins > Question custom fields" in site administration
And I click on "[data-role='deletecategory']" "css_element"
And I click on "Yes" "button" in the "Confirm" "dialogue"
And I should not see "Category for test" in the "#customfield_catlist" "css_element"
And I am on the "Test quiz Q001" "mod_quiz > question bank" page logged in as "teacher1"
And I apply question bank filter "Category" with value "Question category 1"
And I should see "Question bank"
@@ -0,0 +1,407 @@
<?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_columnsortorder;
defined('MOODLE_INTERNAL') || die();
use advanced_testcase;
use context_course;
use core_question\local\bank\column_base;
use core_question\local\bank\question_edit_contexts;
use core_question\local\bank\view;
use moodle_url;
global $CFG;
require_once($CFG->dirroot . '/question/tests/fixtures/testable_core_question_column.php');
require_once($CFG->dirroot . '/question/classes/external.php');
/**
* Test class for columnsortorder feature.
*
* @package qbank_columnsortorder
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Ghaly Marc-Alexandre <marc-alexandreghaly@catalyst-ca.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \qbank_columnsortorder\column_manager
*/
class column_manager_test extends advanced_testcase {
/**
* Generate a course and return a question bank view for the course context.
*
* @return view
*/
protected static function get_question_bank(): view {
$course = self::getDataGenerator()->create_course();
$questionbank = new view(
new question_edit_contexts(context_course::instance($course->id)),
new moodle_url('/'),
$course
);
return $questionbank;
}
/**
* Return an array of visible columns for the question bank.
*
* @return array
*/
protected static function get_columns(): array {
$questionbank = self::get_question_bank();
$columns = [];
foreach ($questionbank->get_visiblecolumns() as $column) {
$columns[] = $column->get_column_id();
}
return $columns;
}
/**
* Provide examples for testing each column setting function, with test data and data format.
*
* @return array[]
*/
public static function settings_provider(): array {
return [
'Test set_column_order' => [
'setting' => 'enabledcol',
'function' => 'set_column_order',
'datamethod' => [__CLASS__, 'get_columns'],
'csv' => true,
],
'Test set_hidden_columns' => [
'setting' => 'hiddencols',
'function' => 'set_hidden_columns',
'datamethod' => [__CLASS__, 'get_columns'],
'csv' => true,
],
'Test set_column_size' => [
'setting' => 'colsize',
'function' => 'set_column_size',
'datamethod' => 'random_string',
'csv' => false,
],
];
}
/**
* Retrieve data using the specified method.
* This function is used to retrieve data from various data methods defined within this class.
*
* @param array|string $datamethod This can be either a function name or an array containing the class and method name.
* @return array|string The retrieved data as an array or string, depending on the data method used.
*/
protected function get_data_from_datamethod(array|string $datamethod): array|string {
return call_user_func($datamethod);
}
/**
* Test setting config settings
*
* @dataProvider settings_provider
* @param string $setting The name of the setting being saved
* @param string $function The name of the function being called
* @param array|string $datamethod The property of the test class to pass to the function.
* @param bool $csv True of the data is stored as a comma-separated list.
* @return void
*/
public function test_settings(
string $setting,
string $function,
array|string $datamethod,
bool $csv,
): void {
$data = $this->get_data_from_datamethod($datamethod);
$this->setAdminUser();
$this->resetAfterTest(true);
$this->assertFalse(get_config('qbank_columnsortorder', $setting));
$this->assertEmpty(get_user_preferences('qbank_columnsortorder_' . $setting));
column_manager::{$function}($data, true);
$expected = $csv ? implode(',', $data) : $data;
$this->assertEquals($expected, get_config('qbank_columnsortorder', $setting));
$this->assertEmpty(get_user_preferences('qbank_columnsortorder_' . $setting));
}
/**
* Test passing null clears the corresponding config setting.
*
* @dataProvider settings_provider
* @param string $setting The name of the setting being saved
* @param string $function The name of the function being called
* @param array|string $datamethod The property of the test class to pass to the function.
* @param bool $csv True of the data is stored as a comma-separated list.
* @return void
*/
public function test_reset_settings(
string $setting,
string $function,
array|string $datamethod,
bool $csv,
): void {
$data = $this->get_data_from_datamethod($datamethod);
$this->setAdminUser();
$this->resetAfterTest(true);
$initial = $csv ? implode(',', $data) : $data;
set_config($setting, $initial, 'qbank_columnsortorder');
$this->assertEquals($initial, get_config('qbank_columnsortorder', $setting));
column_manager::{$function}(null, true);
$this->assertFalse(get_config('qbank_columnsortorder', $setting));
}
/**
* Test setting user preferences
*
* @dataProvider settings_provider
* @param string $setting The name of the setting being saved
* @param string $function The name of the function being called
* @param array|string $datamethod The property of the test class to pass to the function.
* @param bool $csv True of the data is stored as a comma-separated list.
* @return void
*/
public function test_settings_user(
string $setting,
string $function,
array|string $datamethod,
bool $csv,
): void {
$this->resetAfterTest(true);
$data = $this->get_data_from_datamethod($datamethod);
$this->assertFalse(get_config('qbank_columnsortorder', $setting));
$this->assertEmpty(get_user_preferences('qbank_columnsortorder_' . $setting));
column_manager::{$function}($data);
$expected = $csv ? implode(',', $data) : $data;
$this->assertFalse(get_config('qbank_columnsortorder', $setting));
$this->assertEquals($expected, get_user_preferences('qbank_columnsortorder_' . $setting));
}
/**
* Test passing null clears the corresponding user preference.
*
* @dataProvider settings_provider
* @param string $setting The name of the setting being saved
* @param string $function The name of the function being called
* @param array|string $datamethod The property of the test class to pass to the function.
* @param bool $csv True of the data is stored as a comma-separated list.
* @return void
*/
public function test_reset_user_settings(
string $setting,
string $function,
array|string $datamethod,
bool $csv,
): void {
$data = $this->get_data_from_datamethod($datamethod);
$this->setAdminUser();
$this->resetAfterTest(true);
$initial = $csv ? implode(',', $data) : $data;
set_user_preference('qbank_columnsortorder_' . $setting, $initial);
$this->assertEquals($initial, get_user_preferences('qbank_columnsortorder_' . $setting));
column_manager::{$function}(null);
$this->assertEmpty(get_user_preferences('qbank_columnsortorder_' . $setting));
}
/**
* Test function get_columns in helper class, that proper data is returned.
*
* @covers ::get_columns
*/
public function test_getcolumns_function(): void {
$this->resetAfterTest(true);
$this->setAdminUser();
$columnmanager = new column_manager(true);
$questionlistcolumns = $columnmanager->get_columns();
$this->assertIsArray($questionlistcolumns);
foreach ($questionlistcolumns as $columnnobject) {
$this->assertObjectHasProperty('class', $columnnobject);
$this->assertObjectHasProperty('name', $columnnobject);
$this->assertObjectHasProperty('colname', $columnnobject);
}
}
/**
* The get_sorted_columns method should return the provided columns sorted according to enabledcol setting.
*
* @return void
*/
public function test_get_sorted_columns(): void {
$this->resetAfterTest(true);
$this->setAdminUser();
$questionbank = $this->get_question_bank();
$columns = $this->get_columns($questionbank);
$neworder = $columns;
shuffle($neworder);
set_config('enabledcol', implode(',', $neworder), 'qbank_columnsortorder');
$columnmanager = new column_manager(true);
$columnstosort = [];
foreach ($columns as $column) {
$columnstosort[$column] = $column;
}
$sortedcolumns = $columnmanager->get_sorted_columns($columnstosort);
$expectedorder = ['core_question\local\bank\checkbox_column' . column_base::ID_SEPARATOR . 'checkbox_column' => 0];
foreach ($neworder as $columnid) {
$expectedorder[$columnid] = $columnid;
}
$this->assertSame($expectedorder, $sortedcolumns);
}
/**
* Test disabled columns are removed from enabledcol setting and added to disabledcol setting.
*
* @return void
*/
public function test_disable_columns(): void {
$this->resetAfterTest(true);
$this->setAdminUser();
$questionbank = $this->get_question_bank();
$columns = $this->get_columns($questionbank);
// Set up enabledcol with all plugins.
set_config('enabledcol', implode(',', $columns), 'qbank_columnsortorder');
$questionbank = $this->get_question_bank();
$columns = $this->get_columns($questionbank);
$columnmanager = new column_manager(true);
$this->assertFalse(get_config('qbank_columnsortorder', 'disabledcol'));
// Disable a random plugin.
$plugincolumns = array_filter($columns, fn($column) => str_starts_with($column, 'qbank_'));
$randomcolumn = $plugincolumns[array_rand($plugincolumns, 1)];
$randomplugin = explode('\\', $randomcolumn)[0];
$columnmanager->disable_columns($randomplugin);
// The enabledcol setting should now contain all columns except the disabled plugin.
$expectedconfig = array_filter($columns, fn($column) => !str_starts_with($column, $randomplugin));
sort($expectedconfig);
$newconfig = explode(',', get_config('qbank_columnsortorder', 'enabledcol'));
sort($newconfig);
$this->assertEquals($expectedconfig, $newconfig);
$this->assertNotContains($randomcolumn, $newconfig);
// The disabledcol setting should only contain columns from the disabled plugin.
$disabledconfig = explode(',', get_config('qbank_columnsortorder', 'disabledcol'));
array_walk($disabledconfig, fn($column) => $this->assertStringStartsWith($randomplugin, $column));
}
/**
* Test enabling and disabling columns through event observers
*
* @covers \qbank_columnsortorder\event\plugin_observer
*/
public function test_plugin_enabled_disabled_observers(): void {
$this->resetAfterTest(true);
$this->setAdminUser();
$questionbank = $this->get_question_bank();
$columns = $this->get_columns($questionbank);
$columnmanager = new column_manager(true);
$neworder = $columnmanager->get_sorted_columns($columns);
shuffle($neworder);
$columnmanager::set_column_order($neworder, true);
// Get the list of enabled columns, excluding core columns (we can't disable those).
$currentconfig = get_config('qbank_columnsortorder', 'enabledcol');
$currentconfig = array_filter(explode(',', $currentconfig), fn($class) => !str_starts_with($class, 'core'));
// Pick a column at random and get its plugin name.
$randomcolumnid = $currentconfig[array_rand($currentconfig, 1)];
[$randomcolumnclass] = explode(column_base::ID_SEPARATOR, $randomcolumnid, 2);
[$randomplugintodisable] = explode('\\', $randomcolumnclass);
$olddisabledconfig = get_config('qbank_columnsortorder', 'disabledcol');
\core\event\qbank_plugin_disabled::create_for_plugin($randomplugintodisable)->trigger();
$newdisabledconfig = get_config('qbank_columnsortorder', 'disabledcol');
$this->assertNotEquals($olddisabledconfig, $newdisabledconfig);
\core\event\qbank_plugin_enabled::create_for_plugin($randomplugintodisable)->trigger();
$newdisabledconfig = get_config('qbank_columnsortorder', 'disabledcol');
$this->assertEmpty($newdisabledconfig);
$enabledconfig = get_config('qbank_columnsortorder', 'enabledcol');
$contains = strpos($enabledconfig, $randomplugintodisable);
$this->assertNotFalse($contains);
$this->assertIsInt($contains);
}
/**
* Test enabled columns are removed from disabledcol setting and added to enabledcol setting.
*
* @return void
*/
public function test_enable_columns(): void {
$this->resetAfterTest(true);
$this->setAdminUser();
$questionbank = $this->get_question_bank();
$columns = $this->get_columns($questionbank);
// Set up disablecol with columns from 2 random plugins, and enabledcol with all other columns.
$plugincolumns = array_filter($columns, fn($column) => str_starts_with($column, 'qbank_'));
$plugins = array_unique(array_map(fn($column) => explode('\\', $column)[0], $plugincolumns));
$randomplugins = array_rand($plugins, 2);
$randomplugin1 = $plugins[$randomplugins[0]];
$randomplugin2 = $plugins[$randomplugins[1]];
$disabledcols = array_filter(
$columns,
fn($column) => str_starts_with($column, $randomplugin1) || str_starts_with($column, $randomplugin2)
);
$enabledcols = array_diff($columns, $disabledcols);
set_config('enabledcol', implode(',', $enabledcols), 'qbank_columnsortorder');
set_config('disabledcol', implode(',', $disabledcols), 'qbank_columnsortorder');
// Enable one of the disabled plugins.
$columnmanager = new column_manager(true);
$columnmanager->enable_columns($randomplugin1);
// The enabledcol setting should now contain all columns except the remaining disabled plugin.
$expectedenabled = array_filter($columns, fn($column) => !str_starts_with($column, $randomplugin2));
$expecteddisabled = array_filter($disabledcols, fn($column) => str_starts_with($column, $randomplugin2));
sort($expectedenabled);
sort($expecteddisabled);
$newenabled = explode(',', get_config('qbank_columnsortorder', 'enabledcol'));
sort($newenabled);
$this->assertEquals($expectedenabled, $newenabled);
$this->assertNotContains(reset($expecteddisabled), $newenabled);
// The disabledcol setting should only contain columns from the remaining disabled plugin.
$newdisabled = explode(',', get_config('qbank_columnsortorder', 'disabledcol'));
array_walk($newdisabled, fn($column) => $this->assertStringStartsWith($randomplugin2, $column));
}
/**
* Test that get_disabled_columns returns names of all the columns in the disabledcol setting
*
* @return void
*/
public function test_get_disabled_columns(): void {
$this->resetAfterTest(true);
$this->setAdminUser();
$questionbank = $this->get_question_bank();
$columns = $this->get_columns($questionbank);
// Set up disablecol with columns from 2 random plugins, and enabledcol with all other columns.
$plugincolumns = array_filter($columns, fn($column) => str_starts_with($column, 'qbank_'));
$randomcolumn = $plugincolumns[array_rand($plugincolumns, 1)];
$randomplugin = explode('\\', $randomcolumn)[0];
$disabledcols = array_filter($columns, fn($column) => str_starts_with($column, $randomplugin));
set_config('disabledcol', implode(',', $disabledcols), 'qbank_columnsortorder');
$columnmanager = new column_manager(true);
$expecteddisablednames = [];
foreach ($disabledcols as $disabledcolid) {
[$columnclass, $columnname] = explode(column_base::ID_SEPARATOR, $disabledcolid, 2);
$columnobject = $columnclass::from_column_name($questionbank, $columnname);
$expecteddisablednames[$disabledcolid] = (object) [
'disabledname' => $columnobject->get_title(),
];
}
$disablednames = $columnmanager->get_disabled_columns();
$this->assertEquals($expecteddisablednames, $disablednames);
}
}
@@ -0,0 +1,67 @@
<?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_columnsortorder\external;
use qbank_columnsortorder\column_manager;
use qbank_columnsortorder\tests\external_function_testcase;
// phpcs:disable moodle.PHPUnit.TestCaseNames.Missing
// This class inherits its test methods from the parent class.
/**
* Unit tests for qbank_columnsortorder external API.
*
* @package qbank_columnsortorder
* @copyright 2023 Catalyst IT Europe Ltd.
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \qbank_columnsortorder\external\set_column_size
*/
class set_column_size_test extends external_function_testcase {
/**
* @var string Fully-qualified external function class to test.
*/
protected $testclass = '\qbank_columnsortorder\external\set_column_size';
/**
* @var string The name of the setting used to store the data.
*/
protected $setting = 'colsize';
/**
* @var bool Whether the data is stored as a comma-separated list.
*/
protected $csv = false;
/**
* Generate a list of random column sizes.
*
* @return array
*/
protected function generate_test_data(): string {
$columnsortorder = new column_manager();
$questionlistcolumns = $columnsortorder->get_columns();
$columnsizes = [];
foreach ($questionlistcolumns as $columnnobject) {
$columnsizes[] = (object)[
'column' => $columnnobject->name,
'width' => rand(1, 100) . 'px',
];
}
return json_encode($columnsizes);
}
}
@@ -0,0 +1,61 @@
<?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_columnsortorder\external;
use qbank_columnsortorder\column_manager;
use qbank_columnsortorder\tests\external_function_testcase;
// phpcs:disable moodle.PHPUnit.TestCaseNames.Missing
// This class inherits its test methods from the parent class.
/**
* Unit tests for qbank_columnsortorder external API.
*
* @package qbank_columnsortorder
* @copyright 2023 Catalyst IT Europe Ltd.
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \qbank_columnsortorder\external\set_columnbank_order
*/
class set_columnbank_order_test extends external_function_testcase {
/**
* @var string Fully-qualified external function class to test.
*/
protected $testclass = '\qbank_columnsortorder\external\set_columnbank_order';
/**
* @var string The name of the setting used to store the data.
*/
protected $setting = 'enabledcol';
/**
* Return a randomly-ordered list of columns.
*
* @return array
*/
protected function generate_test_data(): array {
$columnsortorder = new column_manager();
$questionlistcolumns = $columnsortorder->get_columns();
$columnclasses = [];
foreach ($questionlistcolumns as $columnnobject) {
$columnclasses[] = $columnnobject->class;
}
shuffle($columnclasses);
return $columnclasses;
}
}
@@ -0,0 +1,56 @@
<?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_columnsortorder\external;
use qbank_columnsortorder\column_manager;
use qbank_columnsortorder\tests\external_function_testcase;
// phpcs:disable moodle.PHPUnit.TestCaseNames.Missing
// This class inherits its test methods from the parent class.
/**
* Unit tests for qbank_columnsortorder external API.
*
* @package qbank_columnsortorder
* @copyright 2023 Catalyst IT Europe Ltd.
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \qbank_columnsortorder\external\set_hidden_columns
*/
class set_hidden_columns_test extends external_function_testcase {
/**
* @var string Fully-qualified external function class to test.
*/
protected $testclass = '\qbank_columnsortorder\external\set_hidden_columns';
/**
* @var string The name of the setting used to store the data.
*/
protected $setting = 'hiddencols';
/**
* Return a random list of hidden column names.
*
* @return array
*/
protected function generate_test_data(): array {
$columnsortorder = new column_manager();
$questionlistcolumns = $columnsortorder->get_columns();
$hiddencolumns = array_slice($questionlistcolumns, rand(0, count($questionlistcolumns) - 1));
return array_map(fn($column) => $column->name, $hiddencolumns);
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Version information for qbank_columnsortorder.
*
* @package qbank_columnsortorder
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Ghaly Marc-Alexandre <marc-alexandreghaly@catalyst-ca.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'qbank_columnsortorder';
$plugin->version = 2024042202;
$plugin->requires = 2024041600;
$plugin->maturity = MATURITY_STABLE;
+11
View File
@@ -0,0 +1,11 @@
define("qbank_comment/comment",["exports","core/fragment","core/str","core/modal_events","core/modal_save_cancel"],(function(_exports,_fragment,_str,_modal_events,_modal_save_cancel){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
/**
* Column selector js.
*
* @module qbank_comment/comment
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_fragment=_interopRequireDefault(_fragment),_modal_events=_interopRequireDefault(_modal_events),_modal_save_cancel=_interopRequireDefault(_modal_save_cancel);_exports.init=()=>{const target=document.querySelector("#categoryquestions");null!==target&&target.addEventListener("click",(e=>{e.target.dataset.target&&e.target.dataset.target.includes("questioncommentpreview")&&(async(questionId,courseID,contextId)=>{const args={questionid:questionId,courseid:courseID},modal=await _modal_save_cancel.default.create({title:(0,_str.get_string)("commentheader","qbank_comment"),body:_fragment.default.loadFragment("qbank_comment","question_comment",contextId,args),large:!0,show:!0,buttons:{save:(0,_str.get_string)("addcomment","qbank_comment"),cancel:(0,_str.get_string)("close","qbank_comment")},removeOnClose:!0}),root=modal.getRoot();root.on(_modal_events.default.bodyRendered,(function(){document.querySelectorAll("div.comment-area a")[0].style.display="none"})),root.on("change","#question_comment_version_dropdown",(e=>{args.questionid=e.target.value,modal.setBody(_fragment.default.loadFragment("qbank_comment","question_comment",contextId,args))})),root.on(_modal_events.default.hidden,(()=>location.reload())),root.on(_modal_events.default.save,(function(e){e.preventDefault();const submitlink=document.querySelectorAll("div.comment-area a")[0],textarea=document.querySelectorAll("div.comment-area textarea")[0];textarea.value!=textarea.getAttribute("aria-label")&&""!=textarea.value&&submitlink.click()}))})(e.target.dataset.questionid,e.target.dataset.courseid,e.target.dataset.contextid)}))}}));
//# sourceMappingURL=comment.min.js.map
File diff suppressed because one or more lines are too long
+100
View File
@@ -0,0 +1,100 @@
// 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/>.
/**
* Column selector js.
*
* @module qbank_comment/comment
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Fragment from 'core/fragment';
import {get_string as getString} from 'core/str';
import ModalEvents from 'core/modal_events';
import SaveCancelModal from 'core/modal_save_cancel';
/**
* Event listeners for the module.
*
* @method clickEvent
* @param {Number} questionId
* @param {Number} courseID
* @param {Number} contextId
*/
const commentEvent = async(questionId, courseID, contextId) => {
const args = {
questionid: questionId,
courseid: courseID
};
const modal = await SaveCancelModal.create({
title: getString('commentheader', 'qbank_comment'),
body: Fragment.loadFragment('qbank_comment', 'question_comment', contextId, args),
large: true,
show: true,
buttons: {
save: getString('addcomment', 'qbank_comment'),
cancel: getString('close', 'qbank_comment'),
},
removeOnClose: true,
});
const root = modal.getRoot();
// Don't display the default add comment link in the modal.
root.on(ModalEvents.bodyRendered, function() {
const submitlink = document.querySelectorAll("div.comment-area a")[0];
submitlink.style.display = 'none';
});
// Version selection event.
root.on('change', '#question_comment_version_dropdown', (e) =>{
args.questionid = e.target.value;
modal.setBody(Fragment.loadFragment('qbank_comment', 'question_comment', contextId, args));
});
// Reload the page when the modal is closed.
root.on(ModalEvents.hidden, () => location.reload());
// Handle adding the comment when the button in the modal is clicked.
root.on(ModalEvents.save, function(e) {
e.preventDefault();
const submitlink = document.querySelectorAll("div.comment-area a")[0];
const textarea = document.querySelectorAll("div.comment-area textarea")[0];
// Check there is a valid comment to add, and trigger adding if there is.
if (textarea.value != textarea.getAttribute('aria-label') && textarea.value != '') {
submitlink.click();
}
});
};
/**
* Entrypoint of the js.
*
* @method init
*/
export const init = () => {
const target = document.querySelector('#categoryquestions');
if (target !== null) {
target.addEventListener('click', (e) => {
if (e.target.dataset.target && e.target.dataset.target.includes('questioncommentpreview')) {
// Call for the event listener to listed for clicks in any comment count row.
commentEvent(e.target.dataset.questionid, e.target.dataset.courseid, e.target.dataset.contextid);
}
});
}
};
@@ -0,0 +1,66 @@
<?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/>.
/**
* Provides the information to backup question comments.
*
* @package qbank_comment
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Matt Porritt <mattp@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class backup_qbank_comment_plugin extends \backup_qbank_plugin {
/**
* Returns the comment information to attach to question element.
*/
protected function define_question_plugin_structure() {
// Define the virtual plugin element with the condition to fulfill.
$plugin = $this->get_plugin_element();
// Create one standard named plugin element (the visible container).
$pluginwrapper = new backup_nested_element($this->get_recommended_name());
// Connect the visible container ASAP.
$plugin->add_child($pluginwrapper);
$comments = new backup_nested_element('comments');
$comment = new backup_nested_element('comment', ['id'], ['component', 'commentarea', 'itemid', 'contextid',
'content', 'format', 'userid', 'timecreated']);
$pluginwrapper->add_child($comments);
$comments->add_child($comment);
$comment->set_source_sql(
"SELECT c.*
FROM {comments} c
WHERE c.contextid = :contextid
AND c.component = 'qbank_comment'
AND c.commentarea = 'question'
AND c.itemid = :itemid",
[
'contextid' => backup_helper::is_sqlparam(context_system::instance()->id),
'itemid' => backup::VAR_PARENTID,
]
);
$comment->annotate_ids('user', 'userid');
return $plugin;
}
}
@@ -0,0 +1,58 @@
<?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/>.
/**
* Restore plugin class that provides the necessary information needed to restore comments for questions.
*
* @package qbank_comment
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Matt Porritt <mattp@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class restore_qbank_comment_plugin extends restore_qbank_plugin {
/**
* Returns the paths to be handled by the plugin at question level.
*/
protected function define_question_plugin_structure() {
return [
new restore_path_element('comment', $this->get_pathfor('/comments/comment'))
];
}
/**
* Process the question comments element.
*
* @param array $data The comment data to restore.
*/
public function process_comment($data) {
global $DB, $CFG;
$data = (object)$data;
$newquestionid = $this->get_new_parentid('question');
$questioncreated = (bool) $this->get_mappingid('question_created', $this->get_old_parentid('question'));
if (!$questioncreated) {
// This question already exists in the question bank. Nothing for us to do.
return;
}
if ($CFG->usecomments) {
$data->itemid = $newquestionid;
$DB->insert_record('comments', $data);
}
}
}
@@ -0,0 +1,116 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace qbank_comment;
use core_question\local\bank\column_base;
use question_bank;
/**
* A column to show the number of comments.
*
* @package qbank_comment
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class comment_count_column extends column_base {
/**
* @var bool Comments enabled or not from config.
*/
protected $commentsenabled = true;
/**
* Load javascript module if enabled.
*
* @return void
*/
public function init(): void {
parent::init();
$this->check_comments_status();
if ($this->commentsenabled) {
global $PAGE;
$PAGE->requires->js_call_amd('qbank_comment/comment', 'init');
}
}
/**
* Check if comments is turned on in the system or not.
*/
protected function check_comments_status(): void {
global $CFG;
if (!$CFG->usecomments) {
$this->commentsenabled = false;
}
}
/**
* Get the name of the column, used internally.
*
* @return string
*/
public function get_name(): string {
return 'commentcount';
}
/**
* Get the title of the column that will be displayed.
*
* @return string
*/
public function get_title(): string {
return get_string('commentplural', 'qbank_comment');
}
/**
* Generate the content to be displayed.
*
* @param object $question The question object.
* @param string $rowclasses Classes that can be added.
*/
protected function display_content($question, $rowclasses): void {
global $DB;
$syscontext = \context_system::instance();
$args = [
'component' => 'qbank_comment',
'commentarea' => 'question',
'itemid' => $question->id,
'contextid' => $syscontext->id,
];
$commentcount = $DB->count_records('comments', $args);
$attributes = [];
if (question_has_capability_on($question, 'comment')) {
$target = 'questioncommentpreview_' . $question->id;
$attributes = [
'href' => '#',
'data-target' => $target,
'data-questionid' => $question->id,
'data-courseid' => $this->qbank->course->id,
'data-contextid' => $syscontext->id,
];
}
echo \html_writer::tag('a', $commentcount, $attributes);
}
public function get_extra_classes(): array {
return ['pr-3'];
}
public function get_default_width(): int {
return 150;
}
}
@@ -0,0 +1,51 @@
<?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_comment\event;
/**
* qbank_comment comment created event.
*
* @package qbank_comment
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class comment_created extends \core\event\comment_created {
/**
* Get URL related to the action, null in this case.
*
* @return null
*/
public function get_url() {
return null;
}
/**
* Returns description of what happened.
*
* @return string
*/
public function get_description() {
$a = new \stdClass();
$a->userid = $this->userid;
$a->objectid = $this->objectid;
$a->component = $this->component;
$a->itemid = $this->other['itemid'];
return get_string('comment_added', 'qbank_comment', $a);
}
}
@@ -0,0 +1,51 @@
<?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_comment\event;
/**
* qbank_comment comment deleted event.
*
* @package qbank_comment
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class comment_deleted extends \core\event\comment_deleted {
/**
* Get URL related to the action, null in this case.
*
* @return null
*/
public function get_url() {
return null;
}
/**
* Returns description of what happened.
*
* @return string
*/
public function get_description() {
$a = new \stdClass();
$a->userid = $this->userid;
$a->objectid = $this->objectid;
$a->component = $this->component;
$a->itemid = $this->other['itemid'];
return get_string('comment_removed', 'qbank_comment', $a);
}
}
@@ -0,0 +1,49 @@
<?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_comment\event;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/comment/lib.php');
use core\event\question_deleted;
/**
* Event observer for question deletion
*
* @package qbank_comment
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class question_deleted_observer {
/**
* Delete any comments for the deleted question.
*
* @param question_deleted $event
* @return void
*/
public static function delete_question_comments(question_deleted $event): void {
\comment::delete_comments([
'contextid' => \context_system::instance()->id,
'component' => 'qbank_comment',
'commentarea' => 'question',
'itemid' => $event->objectid,
]);
}
}
@@ -0,0 +1,38 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace qbank_comment\output;
/**
* Class renderer for comment.
*
* @package qbank_comment
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class renderer extends \plugin_renderer_base {
/**
* Render the html fragment for comment modal.
*
* @param array $displaydata
* @return string
*/
public function render_comment_fragment($displaydata): string {
return $this->render_from_template('qbank_comment/comment_modal', $displaydata);
}
}
@@ -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/>.
namespace qbank_comment;
/**
* Class plugin_features is the entrypoint for the columns.
*
* @package qbank_comment
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class plugin_feature extends \core_question\local\bank\plugin_features_base {
/**
* Get the columns provided by this plugin.
*
* @param \core_question\local\bank\view $qbank
* @return comment_count_column[]
*/
public function get_question_columns(\core_question\local\bank\view $qbank): array {
return [
new comment_count_column($qbank)
];
}
}
@@ -0,0 +1,140 @@
<?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_comment\privacy;
use core_privacy\local\metadata\collection;
use core_privacy\local\request\approved_contextlist;
use core_privacy\local\request\contextlist;
use core_privacy\local\request\userlist;
use core_privacy\local\request\approved_userlist;
/**
* Privacy Subsystem for qbank_comment.
*
* @package qbank_comment
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements
// The qbank_comment stores user provided data.
\core_privacy\local\metadata\provider,
\core_privacy\local\request\core_userlist_provider,
// The qbank_comment provides data directly to core.
\core_privacy\local\request\plugin\provider {
/**
* Returns meta data about this system.
*
* @param collection $collection
* @return collection
*/
public static function get_metadata(collection $collection): collection {
return $collection->add_subsystem_link('core_comment', [], 'privacy:metadata:core_comment');
}
/**
* Get the list of contexts that contain user information for the specified user.
*
* @param int $userid
* @return contextlist
*/
public static function get_contexts_for_userid(int $userid): contextlist {
$contextlist = new contextlist();
$sql = "SELECT contextid
FROM {comments}
WHERE component = :component
AND userid = :userid";
$params = [
'area' => 'question',
'component' => 'qbank_comment',
'userid' => $userid
];
$contextlist->add_from_sql($sql, $params);
return $contextlist;
}
/**
* Get the list of users within a specific context.
*
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
*/
public static function get_users_in_context(userlist $userlist) {
$context = $userlist->get_context();
$params = [
'contextid' => $context->id,
'area' => 'question',
'component' => 'qbank_comment'
];
$sql = "SELECT userid as userid
FROM {comments}
WHERE component = :component
AND contextid = :contextid";
$userlist->add_from_sql('userid', $sql, $params);
}
/**
* Export all user data for the specified user, in the specified contexts.
*
* @param approved_contextlist $contextlist
*/
public static function export_user_data(approved_contextlist $contextlist) {
$contexts = $contextlist->get_contexts();
foreach ($contexts as $context) {
\core_comment\privacy\provider::export_comments(
$context,
'qbank_comment',
'question',
0,
[]
);
}
}
/**
* Delete all data for all users in the specified context.
*
* @param \context $context
*/
public static function delete_data_for_all_users_in_context(\context $context) {
\core_comment\privacy\provider::delete_comments_for_all_users($context, 'qbank_comment');
}
/**
* Delete multiple users within a single context.
*
* @param approved_userlist $userlist The approved context and user information to delete information for.
*/
public static function delete_data_for_users(approved_userlist $userlist) {
\core_comment\privacy\provider::delete_comments_for_users($userlist, 'qbank_comment');
}
/**
* Delete all user data for the specified user, in the specified contexts.
*
* @param approved_contextlist $contextlist
*/
public static function delete_data_for_user(approved_contextlist $contextlist) {
\core_comment\privacy\provider::delete_comments_for_user($contextlist, 'qbank_comment');
}
}
+50
View File
@@ -0,0 +1,50 @@
<?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/>.
/**
* Capability definitions for this module.
*
* @package qbank_comment
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Matt Porritt <mattp@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$capabilities = [
// Controls whether users can comment their own questions.
'moodle/question:commentmine' => [
'captype' => 'write',
'contextlevel' => CONTEXT_COURSE,
'archetypes' => [
'editingteacher' => CAP_ALLOW,
'manager' => CAP_ALLOW
],
'clonepermissionsfrom' => 'moodle/question:editmine'
],
// Controls whether users can comment all questions.
'moodle/question:commentall' => [
'captype' => 'write',
'contextlevel' => CONTEXT_COURSE,
'archetypes' => [
'editingteacher' => CAP_ALLOW,
'manager' => CAP_ALLOW
],
'clonepermissionsfrom' => 'moodle/question:editall'
],
];
+33
View File
@@ -0,0 +1,33 @@
<?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/>.
/**
* Question custom fields events
*
* @package qbank_comment
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$observers = [
[
'eventname' => '\core\event\question_deleted',
'callback' => '\qbank_comment\event\question_deleted_observer::delete_question_comments',
]
];
@@ -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/>.
/**
* Strings for component qbank_comment, language 'en'.
*
* @package qbank_comment
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['pluginname'] = 'Question comments';
$string['privacy:metadata:core_comment'] = 'The Question comments question bank plugin enables users to comment on questions.';
// Column.
$string['comment'] = 'Comment';
$string['commentplural'] = 'Comments';
// Modal.
$string['addcomment'] = 'Add comment';
$string['close'] = 'Close';
$string['commentheader'] = 'Question comments';
$string['commentdisabled'] = 'Comments are currently disabled on this site. Please contact your site administrator.';
// Events.
$string['comment_added'] = 'The user with ID \'{$a->userid}\' added the comment with ID \'{$a->objectid}\'
to the \'{$a->component}\' for the question with ID \'{$a->itemid}\'.';
$string['comment_removed'] = 'The user with ID \'{$a->userid}\' deleted the comment with ID \'{$a->objectid}\'
from the \'{$a->component}\' for the question with ID \'{$a->itemid}\'.';
+142
View File
@@ -0,0 +1,142 @@
<?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/>.
/**
* Helper functions and callbacks.
*
* @package qbank_comment
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot. '/comment/lib.php');
/**
* Validate comment parameter before perform other comments actions.
*
* @param stdClass $commentparam
* {
* context => context the context object
* courseid => int course id
* cm => stdClass course module object
* commentarea => string comment area
* itemid => int itemid
* }
* @return boolean
*/
function qbank_comment_comment_validate($commentparam): bool {
if ($commentparam->commentarea != 'question' && $commentparam->component != 'qbank_comment') {
throw new comment_exception('invalidcommentarea');
}
return true;
}
/**
* Running additional permission check on plugins.
*
* @param stdClass $args
* @return array
*/
function qbank_comment_comment_permissions($args): array {
return ['post' => true, 'view' => true];
}
/**
* Validate comment data before displaying comments.
*
* @param array $comments
* @param stdClass $args
* @return array $comments
*/
function qbank_comment_comment_display($comments, $args): array {
if ($args->commentarea != 'question' && $args->component != 'qbank_comment') {
throw new comment_exception('core_question');
}
return $comments;
}
/**
* Comment content for callbacks.
*
* @param question_definition $question
* @param int $courseid
* @return string
*/
function qbank_comment_preview_display($question, $courseid): string {
global $CFG, $PAGE;
if (question_has_capability_on($question, 'comment') && $CFG->usecomments
&& core\plugininfo\qbank::is_plugin_enabled('qbank_comment')) {
\comment::init($PAGE);
$args = new \stdClass;
$args->contextid = context_system::instance()->id; // Static data to bypass comment sql as context is not needed.
$args->courseid = $courseid;
$args->area = 'question';
$args->itemid = $question->id;
$args->component = 'qbank_comment';
$args->notoggle = true;
$args->autostart = true;
$args->displaycancel = false;
$args->linktext = get_string('commentheader', 'qbank_comment');
$comment = new \comment($args);
$comment->set_view_permission(true);
$comment->set_fullwidth();
return $comment->output();
} else {
return '';
}
}
/**
* Question comment fragment callback.
*
* @param array $args
* @return string rendered output
*/
function qbank_comment_output_fragment_question_comment($args): string {
global $USER, $PAGE, $CFG, $DB;
$displaydata = [];
require_once($CFG->dirroot . '/question/engine/bank.php');
$question = question_bank::load_question($args['questionid']);
$quba = question_engine::make_questions_usage_by_activity(
'core_question_preview', context_user::instance($USER->id));
// Just in case of any regression, it should not break the modal, just show the comments.
if (class_exists('\\qbank_previewquestion\\question_preview_options')) {
$options = new \qbank_previewquestion\question_preview_options($question);
$quba->set_preferred_behaviour($options->behaviour);
$slot = $quba->add_question($question, $options->maxmark);
$quba->start_question($slot, $options->variant);
$transaction = $DB->start_delegated_transaction();
question_engine::save_questions_usage_by_activity($quba);
$transaction->allow_commit();
$displaydata['question'] = $quba->render_question($slot, $options, '1');
}
$displaydata['comment'] = qbank_comment_preview_display($question, $args['courseid']);
$displaydata['commenstdisabled'] = false;
if (empty($displaydata['comment']) && !$CFG->usecomments) {
$displaydata['commenstdisabled'] = true;
}
$selector = \core_question\output\question_version_selection::make_for_question('question_comment_version_dropdown',
$args['questionid']);
$qbankrenderer = $PAGE->get_renderer('core_question', 'bank');
$displaydata['versionselection'] = $selector->export_for_template($qbankrenderer);
return $PAGE->get_renderer('qbank_comment')->render_comment_fragment($displaydata);
}
+5
View File
@@ -0,0 +1,5 @@
/* Comment text area size to maximum */
.question-comment-view .comment-ctrl .comment-area {
max-width: none;
width: 100%;
}
@@ -0,0 +1,49 @@
{{!
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_comment/comment_modal
The template that renders the modal window for adding comments to questions.
* question - The html of the question.
* comment - The html of the comment.
* commenstdisabled - If true display the comment disabled notification.
Example context (json):
{
"commentdata": [
{
"question": "question html",
"comment": "comment html",
"commenstdisabled": "bool"
}
]
}
}}
{{#versionselection}}
{{> core_question/question_version_selection }}
{{/versionselection}}
<div class="question-comment-preview">
{{{question}}}
</div>
<div class="question-comment-view">
{{{comment}}}
{{#commenstdisabled}}
<a class="alert-danger">
{{#str}} commentdisabled, qbank_comment {{/str}}
</a>
{{/commenstdisabled}}
</div>
+216
View File
@@ -0,0 +1,216 @@
<?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_comment;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
require_once($CFG->dirroot. '/comment/lib.php');
/**
* Question comment backup and restore unit tests.
*
* @package qbank_comment
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Matt Porritt <mattp@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class backup_test extends \advanced_testcase {
/**
* @var array Data object for generating a question.
*/
protected $question1data;
/**
* @var array Data object for generating a question.
*/
protected $question2data;
/**
* @var component_generator_base Question Generator.
*/
protected $qgen;
/**
* @var core_course_category Course category.
*/
protected $category;
/**
* @var stdClass Course object.
*/
protected $course;
/**
* Set up
*/
protected function setUp(): void {
parent::setUp();
$this->resetAfterTest();
$this->setAdminUser();
// Set up custom fields.
$data = new \stdClass();
$data->component = 'qbank_comment';
$data->area = 'question';
// Question initial set up.
$this->category = $this->getDataGenerator()->create_category();
$this->course = $this->getDataGenerator()->create_course(['category' => $this->category->id]);
$context = \context_coursecat::instance($this->category->id);
$this->qgen = $this->getDataGenerator()->get_plugin_generator('core_question');
$qcat = $this->qgen->create_question_category(['contextid' => $context->id]);
$this->question1data = ['category' => $qcat->id, 'idnumber' => 'q1'];
$this->question2data = ['category' => $qcat->id, 'idnumber' => 'q2'];
}
/**
* Makes a backup of the course.
*
* @param \stdClass $course The course object.
* @return string Unique identifier for this backup.
*/
protected function backup_course(\stdClass $course): string {
global $CFG, $USER;
// Turn off file logging, otherwise it can't delete the file (Windows).
$CFG->backup_file_logger_level = \backup::LOG_NONE;
// Do backup with default settings. MODE_IMPORT means it will just
// create the directory and not zip it.
$bc = new \backup_controller(\backup::TYPE_1COURSE, $course->id,
\backup::FORMAT_MOODLE, \backup::INTERACTIVE_NO, \backup::MODE_IMPORT,
$USER->id);
$backupid = $bc->get_backupid();
$bc->execute_plan();
$bc->destroy();
return $backupid;
}
/**
* Restores a backup that has been made earlier.
*
* @param string $backupid The unique identifier of the backup.
* @param string $fullname Full name of the new course that is going to be created.
* @param string $shortname Short name of the new course that is going to be created.
* @param int $categoryid The course category the backup is going to be restored in.
* @return int The new course id.
*/
protected function restore_course(string $backupid, string $fullname, string $shortname, int $categoryid): int {
global $CFG, $USER;
// Turn off file logging, otherwise it can't delete the file (Windows).
$CFG->backup_file_logger_level = \backup::LOG_NONE;
// Do restore to new course with default settings.
$newcourseid = \restore_dbops::create_new_course($fullname, $shortname, $categoryid);
$rc = new \restore_controller($backupid, $newcourseid,
\backup::INTERACTIVE_NO, \backup::MODE_GENERAL, $USER->id,
\backup::TARGET_NEW_COURSE);
$rc->execute_precheck();
$rc->execute_plan();
$rc->destroy();
return $newcourseid;
}
/**
* Test comments attached to questions persist
* across the backup and restore process.
*/
public function test_backup_restore(): void {
global $DB, $CFG;
require_once($CFG->dirroot . '/comment/lib.php');
$this->resetAfterTest();
$this->setAdminUser();
$courseshortname = $this->course->shortname;
$coursefullname = $this->course->fullname;
// Create 2 questions.
$question1 = $this->qgen->create_question('shortanswer', null, $this->question1data);
$question2 = $this->qgen->create_question('shortanswer', null, $this->question2data);
// Add comments to the questions.
$args = new \stdClass;
$args->context = \context_system::instance();
$args->course = $this->course;
$args->area = 'question';
$args->itemid = $question1->id;
$args->component = 'qbank_comment';
$args->linktext = get_string('commentheader', 'qbank_comment');
$args->notoggle = true;
$args->autostart = true;
$args->displaycancel = false;
// Two comments for question 1.
$commentobj1 = new \comment($args);
$commentobj1->add('new \comment for question 1 _ 1');
$comment1 = $commentobj1->add('new \comment for question 1 _ 2');
// One comment for question 2.
$args->itemid = $question2->id;
$commentobj2 = new \comment($args);
$comment2 = $commentobj2->add('new \comment for question 2');
// Create a quiz and the questions to that.
$quiz = $this->getDataGenerator()->create_module(
'quiz', ['course' => $this->course->id, 'name' => 'restored_quiz']);
quiz_add_quiz_question($question1->id, $quiz);
quiz_add_quiz_question($question2->id, $quiz);
// Backup the course.
$backupid = $this->backup_course($this->course);
// Now delete everything.
delete_course($this->course, false);
question_delete_question($question1->id);
question_delete_question($question2->id);
// Check the comment data for the questions has also gone.
$DB->record_exists('comments', ['id' => $comment1->id]);
$this->assertFalse($DB->record_exists('comments', ['id' => $comment1->id]));
$this->assertFalse($DB->record_exists('comments', ['id' => $comment2->id]));
// Restore the backup we had made earlier into a new course.
$newcategory = $this->getDataGenerator()->create_category();
$this->restore_course($backupid, $coursefullname, $courseshortname . '_2', $newcategory->id);
// The questions and their associated comments should have been restored.
$sql =
'SELECT q.*
FROM {question} q
JOIN {question_versions} qv ON qv.questionid = q.id
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
WHERE qbe.idnumber = ?';
$newquestion1 = $DB->get_record_sql($sql, ['idnumber' => 'q1']);
$args->itemid = $newquestion1->id;
$commentobj = new \comment($args);
$this->assertEquals($commentobj->count(), 2);
$newquestion2 = $DB->get_record_sql($sql, ['idnumber' => 'q2']);
$args->itemid = $newquestion2->id;
$commentobj = new \comment($args);
$this->assertEquals($commentobj->count(), 1);
}
}
@@ -0,0 +1,168 @@
<?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/>.
/**
* Commenting system steps definitions for question.
*
* @package qbank_comment
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
require_once(__DIR__ . '/../../../../../lib/behat/behat_base.php');
require_once(__DIR__ . '/../../../../tests/behat/behat_question_base.php');
use Behat\Mink\Exception\ExpectationException as ExpectationException,
Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException;
/**
* Steps definitions to deal with the commenting system in question.
*
* @package qbank_comment
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_qbank_comment extends behat_question_base {
/**
* Looks for a table, then looks for a row that contains the given text.
* Once it finds the right row, it clicks a link in that row.
*
* @When I click :arg1 on the row on the comments column
* @param string $linkname
* @param string $rowtext
*/
public function i_click_on_the_row_containing($linkname) {
$exception = new ElementNotFoundException($this->getSession(),
'Cannot find any row on the page containing the text ' . $linkname);
$row = $this->find('css', sprintf('table tbody tr td.commentcount a:contains("%s")', $linkname), $exception);
$row->click();
}
/**
* Looks for the appropriate comment count in the column.
*
* @Then I should see :arg1 on the comments column
* @param string $linkdata
*/
public function i_should_see_on_the_column($linkdata) {
$exception = new ElementNotFoundException($this->getSession(),
'Cannot find any row with the comment count of ' . $linkdata . ' on the column named Comments');
$this->find('css', sprintf('table tbody tr td.commentcount a:contains("%s")', $linkdata), $exception);
}
/**
* Adds the specified option to the question comments of the current modal.
*
* @Then I add :arg1 comment to question
* @param string $comment
*/
public function i_add_comment_to_question($comment) {
// Getting the textarea and setting the provided value.
$exception = new ElementNotFoundException($this->getSession(), 'Question ');
if ($this->running_javascript()) {
$commentstextarea = $this->find('css',
'.modal-dialog .question-comment-view .comment-area textarea', $exception);
$commentstextarea->setValue($comment);
// We delay 1 second which is all we need.
$this->getSession()->wait(1000);
} else {
throw new ExpectationException('JavaScript not running', $this->getSession());
}
}
/**
* Adds the specified option to the question comments of the question preview.
*
* @Then I add :arg1 comment to question preview
* @param string $comment
*/
public function i_add_comment_to_question_preview($comment) {
// Getting the textarea and setting the provided value.
$exception = new ElementNotFoundException($this->getSession(), 'Question ');
if ($this->running_javascript()) {
$commentstextarea = $this->find('css',
'.comment-area textarea', $exception);
$commentstextarea->setValue($comment);
// We delay 1 second which is all we need.
$this->getSession()->wait(1000);
} else {
throw new ExpectationException('JavaScript not running', $this->getSession());
}
}
/**
* Deletes the specified comment from the current question comment preview.
*
* @Then I delete :arg comment from question preview
* @param string $comment
*/
public function i_delete_comment_from_question_preview($comment) {
$exception = new ElementNotFoundException($this->getSession(), '"' . $comment . '" comment ');
// Using xpath liternal to avoid possible problems with comments containing quotes.
$commentliteral = behat_context_helper::escape($comment);
$commentxpath = "//*[contains(concat(' ', normalize-space(@class), ' '), ' comment-ctrl ')]" .
"/descendant::div[@class='comment-message'][contains(., $commentliteral)]";
// Click on delete icon.
$this->execute('behat_general::i_click_on_in_the',
["Delete comment posted by", "icon", $this->escape($commentxpath), "xpath_element"]
);
// Wait for the animation to finish, in theory is just 1 sec, adding 4 just in case.
$this->getSession()->wait(4 * 1000);
}
/**
* Deletes the specified comment from the current question comment modal.
*
* @Then I delete :arg comment from question
* @param string $comment
*/
public function i_delete_comment_from_question($comment) {
$exception = new ElementNotFoundException($this->getSession(), '"' . $comment . '" comment ');
// Using xpath liternal to avoid possible problems with comments containing quotes.
$commentliteral = behat_context_helper::escape($comment);
$commentxpath = "//*[contains(concat(' ', normalize-space(@class), ' '), ' question-comment-view ')]" .
"/descendant::div[@class='comment-message'][contains(., $commentliteral)]";
// Click on delete icon.
$this->execute('behat_general::i_click_on_in_the',
["Delete comment posted by", "icon", $this->escape($commentxpath), "xpath_element"]
);
// Wait for the animation to finish, in theory is just 1 sec, adding 4 just in case.
$this->getSession()->wait(4 * 1000);
}
}
@@ -0,0 +1,151 @@
@qbank @qbank_comment @javascript
Feature: A Teacher can comment in a question
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | T1 | Teacher1 | teacher1@example.com |
| teacher2 | T2 | Teacher2 | teacher2@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| teacher2 | C1 | editingteacher |
And the following "activities" exist:
| activity | name | course | idnumber |
| quiz | Test quiz | C1 | quiz1 |
And the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Test questions |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | First question | Answer the first question |
@javascript
Scenario: Add a comment in question
Given I am on the "Test quiz" "mod_quiz > question bank" page logged in as "teacher1"
And I apply question bank filter "Category" with value "Test questions"
And I should see "0" on the comments column
When I click "0" on the row on the comments column
And I add "Super test comment 01" comment to question
And I click on "Add comment" "button" in the ".modal-dialog" "css_element"
And I should see "Super test comment 01"
And I click on "Close" "button" in the ".modal-dialog" "css_element"
Then I should see "1" on the comments column
@javascript
Scenario: Delete a comment from question
Given I am on the "Test quiz" "mod_quiz > question bank" page logged in as "teacher1"
And I apply question bank filter "Category" with value "Test questions"
And I should see "0" on the comments column
When I click "0" on the row on the comments column
And I add "Super test comment 01 to be deleted" comment to question
And I click on "Add comment" "button" in the ".modal-dialog" "css_element"
And I should see "Super test comment 01 to be deleted"
And I click on "Close" "button" in the ".modal-dialog" "css_element"
Then I should see "1" on the comments column
And I click "1" on the row on the comments column
And I delete "Super test comment 01 to be deleted" comment from question
And I should not see "Super test comment 01 to be deleted"
And I click on "Close" "button" in the ".modal-dialog" "css_element"
But I should see "0" on the comments column
@javascript
Scenario: Preview question with comments
Given I am on the "Test quiz" "mod_quiz > question bank" page logged in as "teacher1"
And I apply question bank filter "Category" with value "Test questions"
And I choose "Preview" action for "First question" in the question bank
And I click on "Comments" "link"
Then I should see "Save comment"
And I add "Super test comment 01" comment to question preview
And I click on "Save comment" "link"
And I wait "1" seconds
Then I should see "Super test comment 01"
And I click on "Close preview" "button"
Then I should see "1" on the comments column
And I choose "Preview" action for "First question" in the question bank
And I click on "Comments" "link"
And I delete "Super test comment 01" comment from question preview
And I should not see "Super test comment 01"
And I click on "Close preview" "button"
Then I should see "0" on the comments column
@javascript
Scenario: Teacher with comment permissions for their own questions but not others questions
Given the following "role capability" exists:
| role | editingteacher |
| moodle/question:commentmine | allow |
| moodle/question:commentall | prevent |
And I am on the "Test quiz" "mod_quiz > question bank" page logged in as "teacher1"
And I apply question bank filter "Category" with value "Test questions"
And I choose "Preview" action for "First question" in the question bank
Then I should not see "Save comment"
And I click on "Close preview" "button"
Then I click on "Create a new question ..." "button"
And I set the field "item_qtype_essay" to "1"
And I press "submitbutton"
Then I should see "Adding an Essay question"
And I set the field "Question name" to "Essay 01 new"
And I set the field "Question text" to "Please write 200 words about Essay 01"
And I press "id_submitbutton"
Then I should see "Essay 01 new"
And I choose "Preview" action for "Essay 01 new" in the question bank
And I click on "Comments" "link"
Then I should see "Save comment"
And I log out
And I am on the "Test quiz" "mod_quiz > question bank" page logged in as "teacher2"
And I apply question bank filter "Category" with value "Test questions"
And I choose "Preview" action for "First question" in the question bank
Then I should not see "Save comment"
And I click on "Close preview" "button"
And I choose "Preview" action for "Essay 01 new" in the question bank
Then I should not see "Save comment"
And I click on "Close preview" "button"
@javascript
Scenario: Comments added from the quiz page are visible
Given I am on the "Test quiz" "mod_quiz > edit" page logged in as "teacher1"
And I press "Add"
And I follow "from question bank"
And I click on "Select" "checkbox" in the "First question" "table_row"
And I click on "Add selected questions to the quiz" "button"
And I click on "Preview question" "link"
And I switch to "questionpreview" window
And I press "Comments"
And I set the field "content" to "Some new comment"
And I click on "Save comment" "link"
And I should see "Some new comment"
And I switch to the main window
And I am on the "Test quiz" "mod_quiz > question bank" page
And I choose "Preview" action for "First question" in the question bank
And I click on "Comments" "link"
And I should see "Some new comment"
And I should see "T1 Teacher1"
And I delete "Some new comment" comment from question preview
And I should not see "Some new comment"
And I am on the "Test quiz" "mod_quiz > edit" page
And I click on "Preview question" "link"
And I switch to "questionpreview" window
And I press "Comments"
Then I should not see "Some new comment"
@javascript
Scenario: Comments modal can change the version using dropdown
Given I log in as "teacher1"
And I am on the "Test quiz" "quiz activity" page
When I navigate to "Question bank" in current page administration
And I should see "First question"
And I choose "Edit question" action for "First question" in the question bank
And I set the field "id_name" to "Renamed question v2"
And I set the field "id_questiontext" to "edited question"
And I press "id_submitbutton"
And I should not see "First question"
And I should see "Renamed question v2"
And I click "0" on the row on the comments column
And I should see "Version 2"
Then I should see "edited question"
And I should see "Version 1"
And I set the field "question_version_dropdown" to "Version 1"
And I should see "Answer the first question"
@@ -0,0 +1,30 @@
@qbank @qbank_comment
Feature: Use the qbank plugin manager page for comment
In order to check the plugin behaviour with enable and disable
Background:
Given the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "activities" exist:
| activity | name | course | idnumber |
| quiz | Test quiz | C1 | quiz1 |
And the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Test questions |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | First question | Answer the first question |
@javascript
Scenario: Enable/disable comment column from the base view
Given I log in as "admin"
When I navigate to "Plugins > Question bank plugins > Manage question bank plugins" in site administration
And I should see "Question comments"
And I click on "Disable" "link" in the "Question comments" "table_row"
And I am on the "Test quiz" "mod_quiz > question bank" page
Then "#categoryquestions .header.commentcount" "css_element" should not be visible
And I navigate to "Plugins > Question bank plugins > Manage question bank plugins" in site administration
And I click on "Enable" "link" in the "Question comments" "table_row"
And I am on the "Test quiz" "mod_quiz > question bank" page
And "#categoryquestions .header.commentcount" "css_element" should be visible
@@ -0,0 +1,127 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace qbank_comment\event;
use advanced_testcase;
use cache;
use comment;
use context;
use context_course;
use core_question_generator;
use stdClass;
/**
* Event tests for question comments.
*
* @package qbank_comment
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class comment_created_deleted_test extends advanced_testcase {
/** @var stdClass Keeps course object */
private $course;
/** @var context Keeps context */
private $context;
/** @var stdClass Keeps question object */
private $questiondata;
/** @var stdClass Keeps comment object */
private $comment;
/**
* Setup test data.
*/
public function setUp(): void {
global $CFG;
require_once($CFG->dirroot . '/comment/lib.php');
$this->resetAfterTest();
$this->setAdminUser();
$generator = $this->getDataGenerator();
/** @var core_question_generator $questiongenerator */
$questiongenerator = $generator->get_plugin_generator('core_question');
// Create a course.
$this->course = $generator->create_course();
$this->context = context_course::instance($this->course->id);
// Create a question in the default category.
$contexts = new \core_question\local\bank\question_edit_contexts($this->context);
$cat = question_make_default_categories($contexts->all());
$this->questiondata = $questiongenerator->create_question('numerical', null,
['name' => 'Example question', 'category' => $cat->id]);
// Ensure the question is not in the cache.
$cache = cache::make('core', 'questiondata');
$cache->delete($this->questiondata->id);
// Comment on question.
$args = new stdClass;
$args->context = $this->context;
$args->course = $this->course;
$args->area = 'question';
$args->itemid = $this->questiondata->id;
$args->component = 'qbank_comment';
$args->linktext = get_string('commentheader', 'qbank_comment');
$args->notoggle = true;
$args->autostart = true;
$args->displaycancel = false;
$this->comment = new comment($args);
}
/**
* Test comment_created event.
*/
public function test_comment_created(): void {
// Triggering and capturing the event.
$sink = $this->redirectEvents();
$this->comment->add('New comment');
$events = $sink->get_events();
$this->assertCount(1, $events);
$event = reset($events);
// Checking that the event contains the expected values.
$this->assertInstanceOf('\qbank_comment\event\comment_created', $event);
$this->assertEquals($this->context, $event->get_context());
$this->assertStringContainsString('\'qbank_comment\' for the question with ID \''.$this->questiondata->id.'\'',
$event->get_description());
}
/**
* Test comment_created event.
*/
public function test_comment_deleted(): void {
// Triggering and capturing the event.
$newcomment = $this->comment->add('New comment to delete');
$sink = $this->redirectEvents();
$this->comment->delete($newcomment->id);
$events = $sink->get_events();
$this->assertCount(1, $events);
$event = reset($events);
// Checking that the event contains the expected values.
$this->assertInstanceOf('\qbank_comment\event\comment_deleted', $event);
$this->assertEquals($this->context, $event->get_context());
$this->assertStringContainsString('\'qbank_comment\' for the question with ID \''.$this->questiondata->id.'\'',
$event->get_description());
}
}
@@ -0,0 +1,66 @@
<?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_comment\event;
/**
* Tests for question_deleted_observer
*
* @package qbank_comment
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \qbank_comment\event\question_deleted_observer
*/
class question_deleted_observer_test extends \advanced_testcase {
/**
* Deleting a question with comments should also delete the comments
*
* @return void
*/
public function test_delete_question_with_comments(): void {
$this->resetAfterTest();
$this->setAdminUser();
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
[, , , $questions] = $questiongenerator->setup_course_and_questions();
$question = reset($questions);
$context = \context_system::instance();
$commentgenerator = $this->getDataGenerator()->get_plugin_generator('core_comment');
/** @var \comment $comment */
$comment = $commentgenerator->create_comment([
'context' => $context,
'component' => 'qbank_comment',
'area' => 'question',
'itemid' => $question->id,
'content' => random_string(),
]);
$this->assertEquals(1, $comment->count());
question_delete_question($question->id);
$newcomment = new \comment((object)[
'context' => $context,
'component' => 'qbank_comment',
'area' => 'question',
'itemid' => $question->id,
]);
$this->assertEquals(0, $newcomment->count());
}
}
+128
View File
@@ -0,0 +1,128 @@
<?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_comment;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/question/bank/comment/lib.php');
/**
* Comment lib unit tests.
*
* @package qbank_comment
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Matt Porritt <mattp@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class lib_test extends \advanced_testcase {
/**
* Test the comment validation callback.
*/
public function test_qbank_comment_comment_validate(): void {
$commentparams = new \stdClass();
$commentparams->commentarea = 'question';
$commentparams->component = 'qbank_comment';
$isvalid = qbank_comment_comment_validate($commentparams);
$this->assertTrue($isvalid);
$this->expectException('comment_exception');
$commentparams->commentarea = 'core_comment';
$commentparams->component = 'blog_comment';
qbank_comment_comment_validate($commentparams);
}
/**
* Test the comment display callback.
*/
public function test_qbank_comment_comment_display(): void {
$comment = new \stdClass();
$comment->text = 'test';
$comments = [$comment];
$commentparams = new \stdClass();
$commentparams->commentarea = 'question';
$commentparams->component = 'qbank_comment';
$responses = qbank_comment_comment_display($comments, $commentparams);
$this->assertEquals($comment->text, $responses[0]->text);
$this->expectException('comment_exception');
$commentparams->commentarea = 'core_comment';
$commentparams->component = 'blog_comment';
qbank_comment_comment_display($comments, $commentparams);
}
/**
* Test the comment preview callback.
*/
public function test_qbank_comment_preview_display(): void {
$this->resetAfterTest();
global $PAGE;
$PAGE->set_url('/');
// Make a test question.
$category = $this->getDataGenerator()->create_category();
$course = $this->getDataGenerator()->create_course(['category' => $category->id]);
$qgen = $this->getDataGenerator()->get_plugin_generator('core_question');
$context = \context_coursecat::instance($category->id);
$qcat = $qgen->create_question_category(['contextid' => $context->id]);
$question = $qgen->create_question('shortanswer', null, ['category' => $qcat->id, 'idnumber' => 'q1']);
$result = qbank_comment_preview_display($question, $course->id);
// User doesn't have perms so expecting no output.
$this->assertEmpty($result);
// Expect output.
$this->setAdminUser();
$result = qbank_comment_preview_display($question, $course->id);
$this->assertStringContainsString('comment-action-post', $result);
}
/**
* Test the comment preview callback.
*/
public function test_qbank_comment_output_fragment_question_comment(): void {
$this->resetAfterTest();
$this->setAdminUser();
global $PAGE;
$PAGE->set_url('/');
// Make a test question.
$category = $this->getDataGenerator()->create_category();
$course = $this->getDataGenerator()->create_course(['category' => $category->id]);
$qgen = $this->getDataGenerator()->get_plugin_generator('core_question');
$context = \context_coursecat::instance($category->id);
$qcat = $qgen->create_question_category(['contextid' => $context->id]);
$question = $qgen->create_question('shortanswer', null, ['category' => $qcat->id, 'idnumber' => 'q1']);
$args = [
'questionid' => $question->id,
'courseid' => $course->id,
];
$result = qbank_comment_output_fragment_question_comment($args);
// Expect output.
$this->assertStringContainsString('comment-action-post', $result);
}
}
@@ -0,0 +1,358 @@
<?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_comment\privacy;
use comment;
use context;
use context_course;
use core_privacy\local\metadata\collection;
use qbank_comment\privacy\provider;
use core_privacy\local\request\approved_userlist;
use stdClass;
/**
* Privacy api tests.
*
* @package qbank_comment
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider_test extends \core_privacy\tests\provider_testcase {
/** @var stdClass A teacher who is only enrolled in course1. */
protected $teacher1;
/** @var stdClass A teacher who is only enrolled in course2. */
protected $teacher2;
/** @var stdClass A teacher who is enrolled in both course1 and course2. */
protected $teacher3;
/** @var stdClass A test course. */
protected $course1;
/** @var stdClass A test course. */
protected $course2;
/**
* Set up function for tests in this class.
*/
protected function setUp(): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
// Create courses.
$generator = $this->getDataGenerator();
$this->course1 = $generator->create_course();
$this->course2 = $generator->create_course();
// Create and enrol teachers.
$this->teacher1 = $generator->create_user();
$this->teacher2 = $generator->create_user();
$this->teacher3 = $generator->create_user();
$studentrole = $DB->get_record('role', ['shortname' => 'editingteacher']);
$generator->enrol_user($this->teacher1->id, $this->course1->id, $studentrole->id);
$generator->enrol_user($this->teacher2->id, $this->course2->id, $studentrole->id);
$generator->enrol_user($this->teacher3->id, $this->course1->id, $studentrole->id);
$generator->enrol_user($this->teacher3->id, $this->course2->id, $studentrole->id);
}
/**
* Posts a comment on a given context.
*
* @param string $text The comment's text.
* @param context $context The context on which we want to put the comment.
*/
protected function add_comment($text, context $context) {
$args = new stdClass;
$args->context = $context;
$args->area = 'question';
$args->itemid = 0;
$args->component = 'qbank_comment';
$args->linktext = get_string('commentheader', 'qbank_comment');
$args->notoggle = true;
$args->autostart = true;
$args->displaycancel = false;
$comment = new comment($args);
$comment->add($text);
}
/**
* Test for provider::get_metadata().
*/
public function test_get_metadata(): void {
$collection = new collection('qbank_comment');
$newcollection = provider::get_metadata($collection);
$itemcollection = $newcollection->get_collection();
$this->assertCount(1, $itemcollection);
$link = reset($itemcollection);
$this->assertEquals('core_comment', $link->get_name());
$this->assertEmpty($link->get_privacy_fields());
$this->assertEquals('privacy:metadata:core_comment', $link->get_summary());
}
/**
* Test for provider::get_contexts_for_userid() when user had not posted any comments..
*/
public function test_get_contexts_for_userid_no_comment(): void {
$this->setUser($this->teacher1);
$coursecontext1 = context_course::instance($this->course1->id);
$this->add_comment('New comment', $coursecontext1);
$this->setUser($this->teacher2);
$contextlist = provider::get_contexts_for_userid($this->teacher2->id);
$this->assertCount(0, $contextlist);
}
/**
* Test for provider::get_contexts_for_userid().
*/
public function test_get_contexts_for_userid(): void {
$coursecontext1 = context_course::instance($this->course1->id);
$coursecontext2 = context_course::instance($this->course2->id);
$this->setUser($this->teacher3);
$this->add_comment('New comment', $coursecontext1);
$this->add_comment('New comment', $coursecontext1);
$this->add_comment('New comment', $coursecontext2);
$contextlist = provider::get_contexts_for_userid($this->teacher3->id);
$this->assertCount(2, $contextlist);
$contextids = $contextlist->get_contextids();
$this->assertEqualsCanonicalizing([$coursecontext1->id, $coursecontext2->id], $contextids);
}
/**
* Test for provider::export_user_data() when the user has not posted any comments.
*/
public function test_export_for_context_no_comment(): void {
$coursecontext1 = context_course::instance($this->course1->id);
$coursecontext2 = context_course::instance($this->course2->id);
$this->setUser($this->teacher1);
$this->add_comment('New comment', $coursecontext1);
$this->setUser($this->teacher2);
$this->setUser($this->teacher2);
$this->export_context_data_for_user($this->teacher1->id, $coursecontext2, 'qbank_comment');
$writer = \core_privacy\local\request\writer::with_context($coursecontext2);
$this->assertFalse($writer->has_any_data());
}
/**
* Test for provider::export_user_data().
*/
public function test_export_for_context(): void {
$coursecontext1 = context_course::instance($this->course1->id);
$coursecontext2 = context_course::instance($this->course2->id);
$this->setUser($this->teacher3);
$this->add_comment('New comment', $coursecontext1);
$this->add_comment('New comment', $coursecontext1);
$this->add_comment('New comment', $coursecontext2);
// Export all of the data for the context.
$this->export_context_data_for_user($this->teacher3->id, $coursecontext1, 'qbank_comment');
$writer = \core_privacy\local\request\writer::with_context($coursecontext1);
$this->assertTrue($writer->has_any_data());
}
/**
* Test for provider::delete_data_for_all_users_in_context().
*/
public function test_delete_data_for_all_users_in_context(): void {
global $DB;
$coursecontext1 = context_course::instance($this->course1->id);
$coursecontext2 = context_course::instance($this->course2->id);
$this->setUser($this->teacher1);
$this->add_comment('New comment', $coursecontext1);
$this->setUser($this->teacher2);
$this->add_comment('New comment', $coursecontext2);
$this->setUser($this->teacher3);
$this->add_comment('New comment', $coursecontext1);
$this->add_comment('New comment', $coursecontext1);
$this->add_comment('New comment', $coursecontext2);
// Before deletion, we should have 3 comments in $coursecontext1 and 2 comments in $coursecontext2.
$this->assertEquals(
3,
$DB->count_records('comments', ['component' => 'qbank_comment', 'contextid' => $coursecontext1->id])
);
$this->assertEquals(
2,
$DB->count_records('comments', ['component' => 'qbank_comment', 'contextid' => $coursecontext2->id])
);
// Delete data based on context.
provider::delete_data_for_all_users_in_context($coursecontext1);
// After deletion, the comments for $coursecontext1 should have been deleted.
$this->assertEquals(
0,
$DB->count_records('comments', ['component' => 'qbank_comment', 'contextid' => $coursecontext1->id])
);
$this->assertEquals(
2,
$DB->count_records('comments', ['component' => 'qbank_comment', 'contextid' => $coursecontext2->id])
);
}
/**
* Test for provider::delete_data_for_user().
*/
public function test_delete_data_for_user(): void {
global $DB;
$coursecontext1 = context_course::instance($this->course1->id);
$coursecontext2 = context_course::instance($this->course2->id);
$this->setUser($this->teacher1);
$this->add_comment('New comment', $coursecontext1);
$this->setUser($this->teacher2);
$this->add_comment('New comment', $coursecontext2);
$this->setUser($this->teacher3);
$this->add_comment('New comment', $coursecontext1);
$this->add_comment('New comment', $coursecontext1);
$this->add_comment('New comment', $coursecontext2);
// Before deletion, we should have 3 comments in $coursecontext1 and 2 comments in $coursecontext2,
// and 3 comments by student12 in $coursecontext1 and $coursecontext2 combined.
$this->assertEquals(
3,
$DB->count_records('comments', ['component' => 'qbank_comment', 'contextid' => $coursecontext1->id])
);
$this->assertEquals(
2,
$DB->count_records('comments', ['component' => 'qbank_comment', 'contextid' => $coursecontext2->id])
);
$this->assertEquals(
3,
$DB->count_records('comments', ['component' => 'qbank_comment', 'userid' => $this->teacher3->id])
);
$contextlist = new \core_privacy\local\request\approved_contextlist($this->teacher3, 'qbank_comment',
[$coursecontext1->id, $coursecontext2->id]);
provider::delete_data_for_user($contextlist);
// After deletion, the comments for the student12 should have been deleted.
$this->assertEquals(
1,
$DB->count_records('comments', ['component' => 'qbank_comment', 'contextid' => $coursecontext1->id])
);
$this->assertEquals(
1,
$DB->count_records('comments', ['component' => 'qbank_comment', 'contextid' => $coursecontext2->id])
);
$this->assertEquals(
0,
$DB->count_records('comments', ['component' => 'qbank_comment', 'userid' => $this->teacher3->id])
);
}
/**
* Test that only users within a course context are fetched.
*/
public function test_get_users_in_context(): void {
$component = 'qbank_comment';
$coursecontext1 = context_course::instance($this->course1->id);
$coursecontext2 = context_course::instance($this->course2->id);
$userlist1 = new \core_privacy\local\request\userlist($coursecontext1, $component);
provider::get_users_in_context($userlist1);
$this->assertCount(0, $userlist1);
$userlist2 = new \core_privacy\local\request\userlist($coursecontext2, $component);
provider::get_users_in_context($userlist2);
$this->assertCount(0, $userlist2);
$this->setUser($this->teacher3);
$this->add_comment('New comment', $coursecontext1);
$this->add_comment('New comment', $coursecontext2);
$this->setUser($this->teacher1);
$this->add_comment('New comment', $coursecontext1);
// The list of users should contain teacher3 and user1.
provider::get_users_in_context($userlist1);
$this->assertCount(2, $userlist1);
$this->assertTrue(in_array($this->teacher1->id, $userlist1->get_userids()));
$this->assertTrue(in_array($this->teacher3->id, $userlist1->get_userids()));
// The list of users should contain teacher3.
provider::get_users_in_context($userlist2);
$this->assertCount(1, $userlist2);
$expected = [$this->teacher3->id];
$actual = $userlist2->get_userids();
$this->assertEquals($expected, $actual);
}
/**
* Test that data for users in approved userlist is deleted.
*/
public function test_delete_data_for_users(): void {
$component = 'qbank_comment';
$coursecontext1 = context_course::instance($this->course1->id);
$coursecontext2 = context_course::instance($this->course2->id);
$this->setUser($this->teacher3);
$this->add_comment('New comment', $coursecontext1);
$this->add_comment('New comment', $coursecontext2);
$this->setUser($this->teacher1);
$this->add_comment('New comment', $coursecontext1);
$userlist1 = new \core_privacy\local\request\userlist($coursecontext1, $component);
provider::get_users_in_context($userlist1);
$this->assertCount(2, $userlist1);
$userlist2 = new \core_privacy\local\request\userlist($coursecontext2, $component);
provider::get_users_in_context($userlist2);
$this->assertCount(1, $userlist2);
// Convert $userlist1 into an approved_contextlist.
$approvedlist1 = new approved_userlist($coursecontext1, $component, $userlist1->get_userids());
// Delete using delete_data_for_user.
provider::delete_data_for_users($approvedlist1);
// Re-fetch users in coursecontext1.
$userlist1 = new \core_privacy\local\request\userlist($coursecontext1, $component);
provider::get_users_in_context($userlist1);
// The user data in coursecontext1 should be deleted.
$this->assertCount(0, $userlist1);
// Re-fetch users in coursecontext2.
$userlist2 = new \core_privacy\local\request\userlist($coursecontext2, $component);
provider::get_users_in_context($userlist2);
// The user data in coursecontext2 should be still present.
$this->assertCount(1, $userlist2);
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Version information for qbank_comment.
*
* @package qbank_comment
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'qbank_comment';
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
$plugin->maturity = MATURITY_STABLE;

Some files were not shown because too many files have changed in this diff Show More