diff --git a/mod/quiz/classes/question/bank/custom_view.php b/mod/quiz/classes/question/bank/custom_view.php index 694f3bdab7b..572ba8daa1b 100644 --- a/mod/quiz/classes/question/bank/custom_view.php +++ b/mod/quiz/classes/question/bank/custom_view.php @@ -28,7 +28,9 @@ namespace mod_quiz\question\bank; defined('MOODLE_INTERNAL') || die(); use core\output\datafilter; +use core_question\local\bank\column_base; use core_question\local\bank\condition; +use core_question\local\bank\column_manager_base; use core_question\local\bank\question_version_status; use mod_quiz\question\bank\filter\custom_category_condition; use qbank_managecategories\category_condition; @@ -79,7 +81,7 @@ class custom_view extends \core_question\local\bank\view { } } - $this->init_required_columns(); + $this->init_columns($this->wanted_columns(), $this->heading_column()); parent::__construct($contexts, $pageurl, $course, $cm, $params, $extraparams); [$this->quiz, ] = get_module_from_cmid($cm->id); $this->set_quiz_has_attempts(quiz_has_attempts($this->quiz->id)); @@ -87,55 +89,42 @@ class custom_view extends \core_question\local\bank\view { } /** - * Init required columns. + * Just use the base column manager in this view. * * @return void */ - protected function init_required_columns(): void { - // Override core columns. - $this->corequestionbankcolumns = [ - 'add_action_column', - 'checkbox_column', - 'question_type_column', - 'question_name_text_column', - 'preview_action_column' - ]; + protected function init_column_manager(): void { + $this->columnmanager = new column_manager_base(); } /** - * Get class for each question bank columns. + * Don't display plugin controls. * - * @return array + * @param \core\context $context + * @param int $categoryid + * @return string */ - protected function get_class_for_columns(): array { + protected function get_plugin_controls(\core\context $context, int $categoryid): string { + return ''; + } + + protected function get_question_bank_plugins(): array { $questionbankclasscolumns = []; - if (question_get_display_preference('qbshowtext', 0, PARAM_INT, new \moodle_url(''))) { - $this->corequestionbankcolumns[] = 'question_text_row'; - } - foreach ($this->corequestionbankcolumns as $fullname) { - $shortname = $fullname; - if (class_exists('mod_quiz\\question\\bank\\' . $fullname)) { - $fullname = 'mod_quiz\\question\\bank\\' . $fullname; - $questionbankclasscolumns[$shortname] = new $fullname($this); - } else if (class_exists('core_question\\local\\bank\\' . $fullname)) { - $fullname = 'core_question\\local\\bank\\' . $fullname; - $questionbankclasscolumns[$shortname] = new $fullname($this); - } else { - $questionbankclasscolumns[$shortname] = ''; + $customviewcolumns = [ + 'mod_quiz\question\bank\add_action_column' . column_base::ID_SEPARATOR . 'add_action_column', + 'core_question\local\bank\checkbox_column' . column_base::ID_SEPARATOR . 'checkbox_column', + 'qbank_viewquestiontype\question_type_column' . column_base::ID_SEPARATOR . 'question_type_column', + 'mod_quiz\question\bank\question_name_text_column' . column_base::ID_SEPARATOR . 'question_name_text_column', + 'mod_quiz\question\bank\preview_action_column' . column_base::ID_SEPARATOR . 'preview_action_column', + ]; + + foreach ($customviewcolumns as $columnid) { + [$columnclass, $columnname] = explode(column_base::ID_SEPARATOR, $columnid, 2); + if (class_exists($columnclass)) { + $questionbankclasscolumns[$columnid] = $columnclass::from_column_name($this, $columnname); } } - return $questionbankclasscolumns; - } - /** - * Get the list of qbank plugins with available objects for features. - * - * @return array - */ - protected function get_question_bank_plugins(): array { - // The view is too crowded, exclude some columns. - $questionbankclasscolumns = parent::get_question_bank_plugins(); - $questionbankclasscolumns = array_intersect_key($questionbankclasscolumns, array_flip($this->corequestionbankcolumns)); return $questionbankclasscolumns; } diff --git a/mod/quiz/lib.php b/mod/quiz/lib.php index 5c3130d6b56..6fd6e221564 100644 --- a/mod/quiz/lib.php +++ b/mod/quiz/lib.php @@ -2549,15 +2549,22 @@ function mod_quiz_output_fragment_question_data(array $args): string { $thispageurl = new \moodle_url('/mod/quiz/edit.php', ['cmid' => $cmid]); $thiscontext = \context_module::instance($cmid); $contexts = new \core_question\local\bank\question_edit_contexts($thiscontext); + $defaultcategory = question_make_default_categories($contexts->all()); + $params['cat'] = implode(',', [$defaultcategory->id, $defaultcategory->contextid]); + $course = get_course($params['courseid']); [, $cm] = get_module_from_cmid($cmid); + $params['tabname'] = 'questions'; // Custom question bank View. $viewclass = clean_param($args['view'], PARAM_NOTAGS); $questionbank = new $viewclass($contexts, $thispageurl, $course, $cm, $params, $extraparams); // Question table. - return $questionbank->display_questions_table(); + $questionbank->add_standard_search_conditions(); + ob_start(); + $questionbank->display_question_list(); + return ob_get_clean(); } /** diff --git a/mod/quiz/styles.css b/mod/quiz/styles.css index 2a5829a108f..d8ad1aff316 100644 --- a/mod/quiz/styles.css +++ b/mod/quiz/styles.css @@ -801,7 +801,7 @@ table.quizreviewsummary td.cell { margin: 0 0.2em; } -#page-mod-quiz-edit #categoryquestions .questionname, +#page-mod-quiz-edit .question-bank-table .questionname, #page-mod-quiz-edit ul.slots li.activity div.activityinstance .questionname { font-weight: bold; color: #555; @@ -845,91 +845,105 @@ table.quizreviewsummary td.cell { display: none; } -#categoryquestions .header { +#page-mod-quiz-edit .question-bank-table { + width: auto; +} + +#page-mod-quiz-edit .question-bank-table .header { text-align: center; padding: 0 2px; border: 0 none; vertical-align: top; + width: auto; } -#categoryquestions .header.checkbox { +#page-mod-quiz-edit .question-bank-table .header.checkbox { vertical-align: bottom; + text-align: center; } -#categoryquestions .header.qtype { +#page-mod-quiz-edit .question-bank-table .header.qtype .sorters { white-space: nowrap; } -#categoryquestions th .sorters { +#page-mod-quiz-edit .question-bank-table th .sorters { font-weight: normal; font-size: 0.8em; } -#categoryquestions td.modifiername, -#categoryquestions td.creatorname, -#categoryquestions td.questionlastused { +#page-mod-quiz-edit .question-bank-table td.modifiername, +#page-mod-quiz-edit .question-bank-table td.creatorname, +#page-mod-quiz-edit .question-bank-table td.questionlastused { line-height: 1em; } -#categoryquestions td.modifiername span.date, -#categoryquestions td.creatorname span.date { +#page-mod-quiz-edit .question-bank-table td.modifiername span.date, +#page-mod-quiz-edit .question-bank-table td.creatorname span.date { font-weight: normal; font-size: 0.8em; } -table#categoryquestions { - width: 100%; +#page-mod-quiz-edit table.question-bank-table { table-layout: fixed; overflow-x: visible; + border-collapse: separate; + border-spacing: 0; } -#categoryquestions .iconcol { - width: 15px; +#page-mod-quiz-edit .question-bank-table .iconcol { + width: 16px; + text-align: center; + padding: 0; +} +#page-mod-quiz-edit .question-bank-table .iconcol .icon { + width: 16px; +} + +#page-mod-quiz-edit .question-bank-table .checkbox { + width: 32px; text-align: center; padding: 0; } -#categoryquestions .checkbox { - width: 19px; - text-align: center; - padding: 0; -} - -#categoryquestions .editmenu { +#page-mod-quiz-edit .question-bank-table .editmenu { width: 5em; } -#categoryquestions .qtype { +#page-mod-quiz-edit .question-bank-table .qtype { text-align: center; } -#categoryquestions .qtype { +#page-mod-quiz-edit .question-bank-table .qtype { width: 28px; padding: 0; } -#categoryquestions .questiontext { +#page-mod-quiz-edit .question-bank-table .questionnametext { + padding: 0 0.2em; +} + +#page-mod-quiz-edit .question-bank-table .questiontext { position: relative; zoom: 1; - padding-left: 0.3em; + padding: 0.3em; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } -#categoryquestions .questionname { +#page-mod-quiz-edit .question-bank-table .questionname { white-space: nowrap; overflow: hidden; zoom: 1; position: relative; } -#categoryquestions .questiontext p { +#page-mod-quiz-edit .question-bank-table .questiontext p { margin: 0; } -#page-mod-quiz-edit table#categoryquestions td, -#page-mod-quiz-edit table#categoryquestions th { +#page-mod-quiz-edit table.question-bank-table td, +#page-mod-quiz-edit table.question-bank-table th { overflow: hidden; white-space: nowrap; } diff --git a/question/amd/build/filter.min.js b/question/amd/build/filter.min.js index a1e2bdf3a57..551c861a6e1 100644 --- a/question/amd/build/filter.min.js +++ b/question/amd/build/filter.min.js @@ -5,6 +5,6 @@ define("core_question/filter",["exports","core/datafilter","core/notification"," * @module core_question/filter * @copyright 2021 Tomo Tsuyuki * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_datafilter=_interopRequireDefault(_datafilter),_notification=_interopRequireDefault(_notification),_selectors=_interopRequireDefault(_selectors),_templates=_interopRequireDefault(_templates),_fragment=_interopRequireDefault(_fragment);_exports.init=(filterRegionId,defaultcourseid,defaultcategoryid,perpage,contextId,component,callback,view,cmid,pagevars,extraparams)=>{var _document$querySelect,_document$querySelect2;const SELECTORS_QUESTION_CONTAINER_ID="#questionscontainer",SELECTORS_QUESTION_TABLE="#questionscontainer table",SELECTORS_SORT_LINK="#questionscontainer div.sorters a",SELECTORS_PAGINATION_LINK="#questionscontainer a[href].page-link",SELECTORS_LASTCHANGED_FIELD="#questionsubmit input[name=lastchanged]",SELECTORS_BULK_ACTIONS="#bulkactionsui-container input",SELECTORS_EDIT_SWITCH=".editmode-switch-form input[name=setmode]",SELECTORS_EDIT_SWITCH_URL=".editmode-switch-form input[name=pageurl]",filterSet=document.querySelector("#".concat(filterRegionId)),filterCondition={cat:defaultcategoryid,courseid:defaultcourseid,filter:{},jointype:0,qpage:0,qperpage:perpage,sortdata:{},tabname:"questions"},defaultSort=null===(_document$querySelect=document.querySelector(SELECTORS_QUESTION_TABLE))||void 0===_document$querySelect||null===(_document$querySelect2=_document$querySelect.dataset)||void 0===_document$querySelect2?void 0:_document$querySelect2.defaultsort;defaultSort&&(filterCondition.sortData=JSON.parse(defaultSort));const coreFilter=new _datafilter.default(filterSet,((filterdata,pendingPromise)=>{var _document$querySelect3,_document$querySelect4;filterdata&&(filterCondition.jointype=parseInt(filterSet.dataset.filterverb,10),delete filterdata.jointype,filterCondition.filter=filterdata,0!==Object.keys(filterdata).length&&(isNaN(filterCondition.jointype)||(filterdata.jointype=filterCondition.jointype),updateUrlParams(filterdata)));const viewData={view:view,cmid:cmid,filtercondition:JSON.stringify(filterCondition),extraparams:extraparams,filterquery:"",lastchanged:null!==(_document$querySelect3=null===(_document$querySelect4=document.querySelector(SELECTORS_LASTCHANGED_FIELD))||void 0===_document$querySelect4?void 0:_document$querySelect4.value)&&void 0!==_document$querySelect3?_document$querySelect3:null};_fragment.default.loadFragment(component,callback,contextId,viewData).then(((questionhtml,jsfooter)=>{const questionscontainer=document.querySelector(SELECTORS_QUESTION_CONTAINER_ID);return void 0===questionhtml&&(questionhtml=""),void 0===jsfooter&&(jsfooter=""),_templates.default.replaceNodeContents(questionscontainer,questionhtml,jsfooter),pendingPromise&&pendingPromise.resolve(),{questionhtml:questionhtml,jsfooter:jsfooter}})).catch(_notification.default.exception)}));coreFilter.activeFilters={},coreFilter.init();const updateUrlParams=filters=>{const url=new URL(location.href),filterQuery=JSON.stringify(filters);url.searchParams.set("filter",filterQuery),history.pushState(filters,"",url),document.querySelectorAll(SELECTORS_BULK_ACTIONS).forEach((bulkAction=>{const actionUrl=new URL(bulkAction.formAction),returnUrl=new URL(actionUrl.searchParams.get("returnurl"));returnUrl.searchParams.set("filter",filterQuery),actionUrl.searchParams.set("returnurl",returnUrl),bulkAction.formAction=actionUrl}));const editSwitch=document.querySelector(SELECTORS_EDIT_SWITCH);if(editSwitch){const editSwitchUrlInput=document.querySelector(SELECTORS_EDIT_SWITCH_URL),editSwitchUrl=new URL(editSwitchUrlInput.value);editSwitchUrl.searchParams.set("filter",filterQuery),editSwitchUrlInput.value=editSwitchUrl,editSwitch.dataset.pageurl=editSwitchUrl}};let initialFilters;document.addEventListener("click",(e=>{const sortableLink=e.target.closest(SELECTORS_SORT_LINK),paginationLink=e.target.closest(SELECTORS_PAGINATION_LINK),clearLink=e.target.closest(_selectors.default.filterset.actions.resetFilters);if(sortableLink){e.preventDefault();const oldSort=filterCondition.sortdata;filterCondition.sortdata={},filterCondition.sortdata[sortableLink.dataset.sortname]=sortableLink.dataset.sortorder;for(const sortname in oldSort)sortname!==sortableLink.dataset.sortname&&(filterCondition.sortdata[sortname]=oldSort[sortname]);filterCondition.qpage=0,coreFilter.updateTableFromFilter()}if(paginationLink){e.preventDefault();const paginationURL=new URL(paginationLink.getAttribute("href")),qpage=paginationURL.searchParams.get("qpage");null!==paginationURL.search&&(filterCondition.qpage=qpage,coreFilter.updateTableFromFilter())}clearLink&&(()=>{const queryString=location.search,urlParams=new URLSearchParams(queryString);if(urlParams.has("cmid")){const cleanedUrl=new URL(location.href.replace(location.search,""));cleanedUrl.searchParams.set("cmid",urlParams.get("cmid")),history.pushState({},"",cleanedUrl)}if(urlParams.has("courseid")){const cleanedUrl=new URL(location.href.replace(location.search,""));cleanedUrl.searchParams.set("courseid",urlParams.get("courseid")),history.pushState({},"",cleanedUrl)}})()}));let jointype=null;if((pagevars=JSON.parse(pagevars)).filter&&(initialFilters=pagevars.filter,pagevars.jointype&&(jointype=pagevars.jointype)),0!==Object.entries(initialFilters).length){const emptyFilterRow=filterSet.querySelector(_selectors.default.filterset.regions.emptyFilterRow);emptyFilterRow&&emptyFilterRow.remove();let rowcount=0;for(const urlFilter in initialFilters){if("jointype"===urlFilter){jointype=initialFilters[urlFilter];continue}rowcount+=1;const filterdata={filtertype:urlFilter,values:initialFilters[urlFilter].values,jointype:initialFilters[urlFilter].jointype,filteroptions:initialFilters[urlFilter].filteroptions,rownum:rowcount};coreFilter.addFilterRow(filterdata)}coreFilter.filterSet.dataset.filterverb=jointype;const join=coreFilter.filterSet.querySelector(_selectors.default.filterset.fields.join);join.querySelectorAll('option:not([value="'.concat(jointype,'"])')).forEach((option=>option.remove())),join.disabled=!0}}})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_datafilter=_interopRequireDefault(_datafilter),_notification=_interopRequireDefault(_notification),_selectors=_interopRequireDefault(_selectors),_templates=_interopRequireDefault(_templates),_fragment=_interopRequireDefault(_fragment);_exports.init=(filterRegionId,defaultcourseid,defaultcategoryid,perpage,contextId,component,callback,view,cmid,pagevars,extraparams)=>{var _document$querySelect,_document$querySelect2,_document$querySelect3,_document$querySelect4;const SELECTORS_QUESTION_CONTAINER_ID="#questionscontainer",SELECTORS_QUESTION_TABLE="#questionscontainer table",SELECTORS_SORT_LINK="#questionscontainer div.sorters a",SELECTORS_PAGINATION_LINK="#questionscontainer a[href].page-link",SELECTORS_LASTCHANGED_FIELD="#questionsubmit input[name=lastchanged]",SELECTORS_EDIT_SWITCH=".editmode-switch-form input[name=setmode]",SELECTORS_EDIT_SWITCH_URL=".editmode-switch-form input[name=pageurl]",filterSet=document.querySelector("#".concat(filterRegionId)),viewData={extraparams:extraparams,cmid:cmid,view:view,cat:defaultcategoryid,courseid:defaultcourseid,filter:{},jointype:0,qpage:0,qperpage:perpage,sortdata:{},lastchanged:null!==(_document$querySelect=null===(_document$querySelect2=document.querySelector(SELECTORS_LASTCHANGED_FIELD))||void 0===_document$querySelect2?void 0:_document$querySelect2.value)&&void 0!==_document$querySelect?_document$querySelect:null};let sortData={};const defaultSort=null===(_document$querySelect3=document.querySelector(SELECTORS_QUESTION_TABLE))||void 0===_document$querySelect3||null===(_document$querySelect4=_document$querySelect3.dataset)||void 0===_document$querySelect4?void 0:_document$querySelect4.defaultsort;defaultSort&&(sortData=JSON.parse(defaultSort));const coreFilter=new _datafilter.default(filterSet,((filterdata,pendingPromise)=>{filterdata&&(viewData.jointype=parseInt(filterSet.dataset.filterverb,10),delete filterdata.jointype,viewData.filter=filterdata,0!==Object.keys(filterdata).length&&(isNaN(viewData.jointype)||(filterdata.jointype=viewData.jointype),updateUrlParams(filterdata))),viewData.filter=JSON.stringify(filterdata),viewData.sortdata=JSON.stringify(sortData),_fragment.default.loadFragment(component,callback,contextId,viewData).then(((questionhtml,jsfooter)=>{const questionscontainer=document.querySelector(SELECTORS_QUESTION_CONTAINER_ID);return void 0===questionhtml&&(questionhtml=""),void 0===jsfooter&&(jsfooter=""),_templates.default.replaceNode(questionscontainer,questionhtml,jsfooter),pendingPromise&&pendingPromise.resolve(),{questionhtml:questionhtml,jsfooter:jsfooter}})).catch(_notification.default.exception)}));coreFilter.activeFilters={},coreFilter.init();const updateUrlParams=filters=>{const url=new URL(location.href),filterQuery=JSON.stringify(filters);url.searchParams.set("filter",filterQuery),history.pushState(filters,"",url);const editSwitch=document.querySelector(SELECTORS_EDIT_SWITCH);if(editSwitch){const editSwitchUrlInput=document.querySelector(SELECTORS_EDIT_SWITCH_URL),editSwitchUrl=new URL(editSwitchUrlInput.value);editSwitchUrl.searchParams.set("filter",filterQuery),editSwitchUrlInput.value=editSwitchUrl,editSwitch.dataset.pageurl=editSwitchUrl}};let initialFilters;document.addEventListener("click",(e=>{const sortableLink=e.target.closest(SELECTORS_SORT_LINK),paginationLink=e.target.closest(SELECTORS_PAGINATION_LINK),clearLink=e.target.closest(_selectors.default.filterset.actions.resetFilters);if(sortableLink){e.preventDefault();const oldSort=sortData;sortData={},sortData[sortableLink.dataset.sortname]=sortableLink.dataset.sortorder;for(const sortname in oldSort)sortname!==sortableLink.dataset.sortname&&(sortData[sortname]=oldSort[sortname]);viewData.qpage=0,coreFilter.updateTableFromFilter()}if(paginationLink){e.preventDefault();const paginationURL=new URL(paginationLink.getAttribute("href")),qpage=paginationURL.searchParams.get("qpage");null!==paginationURL.search&&(viewData.qpage=qpage,coreFilter.updateTableFromFilter())}clearLink&&(()=>{const queryString=location.search,urlParams=new URLSearchParams(queryString);if(urlParams.has("cmid")){const cleanedUrl=new URL(location.href.replace(location.search,""));cleanedUrl.searchParams.set("cmid",urlParams.get("cmid")),history.pushState({},"",cleanedUrl)}if(urlParams.has("courseid")){const cleanedUrl=new URL(location.href.replace(location.search,""));cleanedUrl.searchParams.set("courseid",urlParams.get("courseid")),history.pushState({},"",cleanedUrl)}})()}));let jointype=null;if((pagevars=JSON.parse(pagevars)).filter&&(initialFilters=pagevars.filter,pagevars.jointype&&(jointype=pagevars.jointype)),0!==Object.entries(initialFilters).length){const emptyFilterRow=filterSet.querySelector(_selectors.default.filterset.regions.emptyFilterRow);emptyFilterRow&&emptyFilterRow.remove();let rowcount=0;for(const urlFilter in initialFilters){if("jointype"===urlFilter){jointype=initialFilters[urlFilter];continue}rowcount+=1;const filterdata={filtertype:urlFilter,values:initialFilters[urlFilter].values,jointype:initialFilters[urlFilter].jointype,filteroptions:initialFilters[urlFilter].filteroptions,rownum:rowcount};coreFilter.addFilterRow(filterdata)}coreFilter.filterSet.dataset.filterverb=jointype;const join=coreFilter.filterSet.querySelector(_selectors.default.filterset.fields.join);join.querySelectorAll('option:not([value="'.concat(jointype,'"])')).forEach((option=>option.remove())),join.disabled=!0}}})); //# sourceMappingURL=filter.min.js.map \ No newline at end of file diff --git a/question/amd/build/filter.min.js.map b/question/amd/build/filter.min.js.map index 3570de23359..bc59bbb3f50 100644 --- a/question/amd/build/filter.min.js.map +++ b/question/amd/build/filter.min.js.map @@ -1 +1 @@ -{"version":3,"file":"filter.min.js","sources":["../src/filter.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Question bank filter management.\n *\n * @module core_question/filter\n * @copyright 2021 Tomo Tsuyuki \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport CoreFilter from 'core/datafilter';\nimport Notification from 'core/notification';\nimport Selectors from 'core/datafilter/selectors';\nimport Templates from 'core/templates';\nimport Fragment from 'core/fragment';\n\n/**\n * Initialise the question bank filter on the element with the given id.\n *\n * @param {String} filterRegionId ID of the HTML element containing the filters.\n * @param {String} defaultcourseid Course ID for the default course to pass back to the view.\n * @param {String} defaultcategoryid Question bank category ID for the default course to pass back to the view.\n * @param {Number} perpage The number of questions to display per page.\n * @param {Number} contextId Context ID of the question bank view.\n * @param {string} component Frankenstyle name of the component for the fragment API callback (e.g. core_question)\n * @param {string} callback Name of the callback for the fragment API (e.g question_data)\n * @param {string} view The class name of the question bank view class used for this page.\n * @param {Number} cmid If we are in an activitiy, the course module ID.\n * @param {string} pagevars JSON-encoded parameters from passed from the view, including filters and jointype.\n * @param {string} extraparams JSON-encoded additional parameters specific to this view class, used for re-rendering the view.\n */\nexport const init = (\n filterRegionId,\n defaultcourseid,\n defaultcategoryid,\n perpage,\n contextId,\n component,\n callback,\n view,\n cmid,\n pagevars,\n extraparams\n) => {\n\n const SELECTORS = {\n QUESTION_CONTAINER_ID: '#questionscontainer',\n QUESTION_TABLE: '#questionscontainer table',\n SORT_LINK: '#questionscontainer div.sorters a',\n PAGINATION_LINK: '#questionscontainer a[href].page-link',\n LASTCHANGED_FIELD: '#questionsubmit input[name=lastchanged]',\n BULK_ACTIONS: '#bulkactionsui-container input',\n MENU_ACTIONS: '.menu-action',\n EDIT_SWITCH: '.editmode-switch-form input[name=setmode]',\n EDIT_SWITCH_URL: '.editmode-switch-form input[name=pageurl]',\n };\n\n const filterSet = document.querySelector(`#${filterRegionId}`);\n\n const filterCondition = {\n cat: defaultcategoryid,\n courseid: defaultcourseid,\n filter: {},\n jointype: 0,\n qpage: 0,\n qperpage: perpage,\n sortdata: {},\n tabname: 'questions',\n };\n\n const defaultSort = document.querySelector(SELECTORS.QUESTION_TABLE)?.dataset?.defaultsort;\n if (defaultSort) {\n filterCondition.sortData = JSON.parse(defaultSort);\n }\n\n let filterQuery = '';\n\n /**\n * Retrieve table data.\n *\n * @param {Object} filterdata data\n * @param {Promise} pendingPromise pending promise\n */\n const applyFilter = (filterdata, pendingPromise) => {\n // Reload the questions based on the specified filters. If no filters are provided,\n // use the default category filter condition.\n if (filterdata) {\n // Main join types.\n filterCondition.jointype = parseInt(filterSet.dataset.filterverb, 10);\n delete filterdata.jointype;\n // Retrieve filter info.\n filterCondition.filter = filterdata;\n if (Object.keys(filterdata).length !== 0) {\n if (!isNaN(filterCondition.jointype)) {\n filterdata.jointype = filterCondition.jointype;\n }\n updateUrlParams(filterdata);\n }\n }\n // Load questions for first page.\n const viewData = {\n view: view,\n cmid: cmid,\n filtercondition: JSON.stringify(filterCondition),\n extraparams: extraparams,\n filterquery: filterQuery,\n lastchanged: document.querySelector(SELECTORS.LASTCHANGED_FIELD)?.value ?? null\n };\n Fragment.loadFragment(component, callback, contextId, viewData)\n // Render questions for first page and pagination.\n .then((questionhtml, jsfooter) => {\n const questionscontainer = document.querySelector(SELECTORS.QUESTION_CONTAINER_ID);\n if (questionhtml === undefined) {\n questionhtml = '';\n }\n if (jsfooter === undefined) {\n jsfooter = '';\n }\n Templates.replaceNodeContents(questionscontainer, questionhtml, jsfooter);\n // Resolve filter promise.\n if (pendingPromise) {\n pendingPromise.resolve();\n }\n return {questionhtml, jsfooter};\n })\n .catch(Notification.exception);\n };\n\n // Init core filter processor with apply callback.\n const coreFilter = new CoreFilter(filterSet, applyFilter);\n coreFilter.activeFilters = {}; // Unset useless courseid filter.\n coreFilter.init();\n\n /**\n * Update URL Param based upon the current filter.\n *\n * @param {Object} filters Active filters.\n */\n const updateUrlParams = (filters) => {\n const url = new URL(location.href);\n const filterQuery = JSON.stringify(filters);\n url.searchParams.set('filter', filterQuery);\n history.pushState(filters, '', url);\n document.querySelectorAll(SELECTORS.BULK_ACTIONS).forEach(bulkAction => {\n const actionUrl = new URL(bulkAction.formAction);\n const returnUrl = new URL(actionUrl.searchParams.get('returnurl'));\n returnUrl.searchParams.set('filter', filterQuery);\n actionUrl.searchParams.set('returnurl', returnUrl);\n bulkAction.formAction = actionUrl;\n });\n const editSwitch = document.querySelector(SELECTORS.EDIT_SWITCH);\n if (editSwitch) {\n const editSwitchUrlInput = document.querySelector(SELECTORS.EDIT_SWITCH_URL);\n const editSwitchUrl = new URL(editSwitchUrlInput.value);\n editSwitchUrl.searchParams.set('filter', filterQuery);\n editSwitchUrlInput.value = editSwitchUrl;\n editSwitch.dataset.pageurl = editSwitchUrl;\n }\n };\n\n /**\n * Cleans URL parameters.\n */\n const cleanUrlParams = () => {\n const queryString = location.search;\n const urlParams = new URLSearchParams(queryString);\n if (urlParams.has('cmid')) {\n const cleanedUrl = new URL(location.href.replace(location.search, ''));\n cleanedUrl.searchParams.set('cmid', urlParams.get('cmid'));\n history.pushState({}, '', cleanedUrl);\n }\n\n if (urlParams.has('courseid')) {\n const cleanedUrl = new URL(location.href.replace(location.search, ''));\n cleanedUrl.searchParams.set('courseid', urlParams.get('courseid'));\n history.pushState({}, '', cleanedUrl);\n }\n };\n\n // Add listeners for the sorting, paging and clear actions.\n document.addEventListener('click', e => {\n const sortableLink = e.target.closest(SELECTORS.SORT_LINK);\n const paginationLink = e.target.closest(SELECTORS.PAGINATION_LINK);\n const clearLink = e.target.closest(Selectors.filterset.actions.resetFilters);\n if (sortableLink) {\n e.preventDefault();\n const oldSort = filterCondition.sortdata;\n filterCondition.sortdata = {};\n filterCondition.sortdata[sortableLink.dataset.sortname] = sortableLink.dataset.sortorder;\n for (const sortname in oldSort) {\n if (sortname !== sortableLink.dataset.sortname) {\n filterCondition.sortdata[sortname] = oldSort[sortname];\n }\n }\n filterCondition.qpage = 0;\n coreFilter.updateTableFromFilter();\n }\n if (paginationLink) {\n e.preventDefault();\n const paginationURL = new URL(paginationLink.getAttribute(\"href\"));\n const qpage = paginationURL.searchParams.get('qpage');\n if (paginationURL.search !== null) {\n filterCondition.qpage = qpage;\n coreFilter.updateTableFromFilter();\n }\n }\n if (clearLink) {\n cleanUrlParams();\n }\n });\n\n // Run apply filter at page load.\n pagevars = JSON.parse(pagevars);\n let initialFilters;\n let jointype = null;\n if (pagevars.filter) {\n // Load initial filter based on page vars.\n initialFilters = pagevars.filter;\n if (pagevars.jointype) {\n jointype = pagevars.jointype;\n }\n }\n\n if (Object.entries(initialFilters).length !== 0) {\n // Remove the default empty filter row.\n const emptyFilterRow = filterSet.querySelector(Selectors.filterset.regions.emptyFilterRow);\n if (emptyFilterRow) {\n emptyFilterRow.remove();\n }\n\n // Add filters.\n let rowcount = 0;\n for (const urlFilter in initialFilters) {\n if (urlFilter === 'jointype') {\n jointype = initialFilters[urlFilter];\n continue;\n }\n // Add each filter row.\n rowcount += 1;\n const filterdata = {\n filtertype: urlFilter,\n values: initialFilters[urlFilter].values,\n jointype: initialFilters[urlFilter].jointype,\n filteroptions: initialFilters[urlFilter].filteroptions,\n rownum: rowcount\n };\n coreFilter.addFilterRow(filterdata);\n }\n coreFilter.filterSet.dataset.filterverb = jointype;\n\n // Since we must filter by category, it does not make sense to allow the top-level \"match any\" or \"match none\" conditions,\n // as this would exclude the category. Remove those options and disable the select.\n const join = coreFilter.filterSet.querySelector(Selectors.filterset.fields.join);\n join.querySelectorAll(`option:not([value=\"${jointype}\"])`).forEach((option) => option.remove());\n join.disabled = true;\n }\n};\n"],"names":["filterRegionId","defaultcourseid","defaultcategoryid","perpage","contextId","component","callback","view","cmid","pagevars","extraparams","SELECTORS","filterSet","document","querySelector","filterCondition","cat","courseid","filter","jointype","qpage","qperpage","sortdata","tabname","defaultSort","_document$querySelect","dataset","_document$querySelect2","defaultsort","sortData","JSON","parse","coreFilter","CoreFilter","filterdata","pendingPromise","parseInt","filterverb","Object","keys","length","isNaN","updateUrlParams","viewData","filtercondition","stringify","filterquery","lastchanged","_document$querySelect4","value","loadFragment","then","questionhtml","jsfooter","questionscontainer","undefined","replaceNodeContents","resolve","catch","Notification","exception","activeFilters","init","filters","url","URL","location","href","filterQuery","searchParams","set","history","pushState","querySelectorAll","forEach","bulkAction","actionUrl","formAction","returnUrl","get","editSwitch","editSwitchUrlInput","editSwitchUrl","pageurl","initialFilters","addEventListener","e","sortableLink","target","closest","paginationLink","clearLink","Selectors","filterset","actions","resetFilters","preventDefault","oldSort","sortname","sortorder","updateTableFromFilter","paginationURL","getAttribute","search","queryString","urlParams","URLSearchParams","has","cleanedUrl","replace","cleanUrlParams","entries","emptyFilterRow","regions","remove","rowcount","urlFilter","filtertype","values","filteroptions","rownum","addFilterRow","join","fields","option","disabled"],"mappings":";;;;;;;4UA4CoB,CAChBA,eACAC,gBACAC,kBACAC,QACAC,UACAC,UACAC,SACAC,KACAC,KACAC,SACAC,sEAGMC,gCACqB,sBADrBA,yBAEc,4BAFdA,oBAGS,oCAHTA,0BAIe,wCAJfA,4BAKiB,0CALjBA,uBAMY,iCANZA,sBAQW,4CARXA,0BASe,4CAGfC,UAAYC,SAASC,yBAAkBd,iBAEvCe,gBAAkB,CACpBC,IAAKd,kBACLe,SAAUhB,gBACViB,OAAQ,GACRC,SAAU,EACVC,MAAO,EACPC,SAAUlB,QACVmB,SAAU,GACVC,QAAS,aAGPC,0CAAcX,SAASC,cAAcH,2FAAvBc,sBAAkDC,iDAAlDC,uBAA2DC,YAC3EJ,cACAT,gBAAgBc,SAAWC,KAAKC,MAAMP,oBAyDpCQ,WAAa,IAAIC,oBAAWrB,WA9Cd,CAACsB,WAAYC,oEAGzBD,aAEAnB,gBAAgBI,SAAWiB,SAASxB,UAAUc,QAAQW,WAAY,WAC3DH,WAAWf,SAElBJ,gBAAgBG,OAASgB,WACc,IAAnCI,OAAOC,KAAKL,YAAYM,SACnBC,MAAM1B,gBAAgBI,YACvBe,WAAWf,SAAWJ,gBAAgBI,UAE1CuB,gBAAgBR,oBAIlBS,SAAW,CACbpC,KAAMA,KACNC,KAAMA,KACNoC,gBAAiBd,KAAKe,UAAU9B,iBAChCL,YAAaA,YACboC,YA9BU,GA+BVC,0EAAalC,SAASC,cAAcH,sEAAvBqC,uBAAqDC,+DAAS,wBAEtEC,aAAa7C,UAAWC,SAAUF,UAAWuC,UAEjDQ,MAAK,CAACC,aAAcC,kBACXC,mBAAqBzC,SAASC,cAAcH,6CAC7B4C,IAAjBH,eACAA,aAAe,SAEFG,IAAbF,WACAA,SAAW,uBAELG,oBAAoBF,mBAAoBF,aAAcC,UAE5DlB,gBACAA,eAAesB,UAEZ,CAACL,aAAAA,aAAcC,SAAAA,aAEzBK,MAAMC,sBAAaC,cAK5B5B,WAAW6B,cAAgB,GAC3B7B,WAAW8B,aAOLpB,gBAAmBqB,gBACfC,IAAM,IAAIC,IAAIC,SAASC,MACvBC,YAActC,KAAKe,UAAUkB,SACnCC,IAAIK,aAAaC,IAAI,SAAUF,aAC/BG,QAAQC,UAAUT,QAAS,GAAIC,KAC/BnD,SAAS4D,iBAAiB9D,wBAAwB+D,SAAQC,mBAChDC,UAAY,IAAIX,IAAIU,WAAWE,YAC/BC,UAAY,IAAIb,IAAIW,UAAUP,aAAaU,IAAI,cACrDD,UAAUT,aAAaC,IAAI,SAAUF,aACrCQ,UAAUP,aAAaC,IAAI,YAAaQ,WACxCH,WAAWE,WAAaD,mBAEtBI,WAAanE,SAASC,cAAcH,0BACtCqE,WAAY,OACNC,mBAAqBpE,SAASC,cAAcH,2BAC5CuE,cAAgB,IAAIjB,IAAIgB,mBAAmBhC,OACjDiC,cAAcb,aAAaC,IAAI,SAAUF,aACzCa,mBAAmBhC,MAAQiC,cAC3BF,WAAWtD,QAAQyD,QAAUD,oBAyDjCE,eAjCJvE,SAASwE,iBAAiB,SAASC,UACzBC,aAAeD,EAAEE,OAAOC,QAAQ9E,qBAChC+E,eAAiBJ,EAAEE,OAAOC,QAAQ9E,2BAClCgF,UAAYL,EAAEE,OAAOC,QAAQG,mBAAUC,UAAUC,QAAQC,iBAC3DR,aAAc,CACdD,EAAEU,uBACIC,QAAUlF,gBAAgBO,SAChCP,gBAAgBO,SAAW,GAC3BP,gBAAgBO,SAASiE,aAAa7D,QAAQwE,UAAYX,aAAa7D,QAAQyE,cAC1E,MAAMD,YAAYD,QACfC,WAAaX,aAAa7D,QAAQwE,WAClCnF,gBAAgBO,SAAS4E,UAAYD,QAAQC,WAGrDnF,gBAAgBK,MAAQ,EACxBY,WAAWoE,2BAEXV,eAAgB,CAChBJ,EAAEU,uBACIK,cAAgB,IAAIpC,IAAIyB,eAAeY,aAAa,SACpDlF,MAAQiF,cAAchC,aAAaU,IAAI,SAChB,OAAzBsB,cAAcE,SACdxF,gBAAgBK,MAAQA,MACxBY,WAAWoE,yBAGfT,WA3Ce,YACba,YAActC,SAASqC,OACvBE,UAAY,IAAIC,gBAAgBF,gBAClCC,UAAUE,IAAI,QAAS,OACjBC,WAAa,IAAI3C,IAAIC,SAASC,KAAK0C,QAAQ3C,SAASqC,OAAQ,KAClEK,WAAWvC,aAAaC,IAAI,OAAQmC,UAAU1B,IAAI,SAClDR,QAAQC,UAAU,GAAI,GAAIoC,eAG1BH,UAAUE,IAAI,YAAa,OACrBC,WAAa,IAAI3C,IAAIC,SAASC,KAAK0C,QAAQ3C,SAASqC,OAAQ,KAClEK,WAAWvC,aAAaC,IAAI,WAAYmC,UAAU1B,IAAI,aACtDR,QAAQC,UAAU,GAAI,GAAIoC,cAgC1BE,UAOJ3F,SAAW,SAFfV,SAAWqB,KAAKC,MAAMtB,WAGTS,SAETkE,eAAiB3E,SAASS,OACtBT,SAASU,WACTA,SAAWV,SAASU,WAIkB,IAA1CmB,OAAOyE,QAAQ3B,gBAAgB5C,OAAc,OAEvCwE,eAAiBpG,UAAUE,cAAc8E,mBAAUC,UAAUoB,QAAQD,gBACvEA,gBACAA,eAAeE,aAIfC,SAAW,MACV,MAAMC,aAAahC,eAAgB,IAClB,aAAdgC,UAA0B,CAC1BjG,SAAWiE,eAAegC,oBAI9BD,UAAY,QACNjF,WAAa,CACfmF,WAAYD,UACZE,OAASlC,eAAegC,WAAWE,OACnCnG,SAAUiE,eAAegC,WAAWjG,SACpCoG,cAAenC,eAAegC,WAAWG,cACzCC,OAAQL,UAEZnF,WAAWyF,aAAavF,YAE5BF,WAAWpB,UAAUc,QAAQW,WAAalB,eAIpCuG,KAAO1F,WAAWpB,UAAUE,cAAc8E,mBAAUC,UAAU8B,OAAOD,MAC3EA,KAAKjD,8CAAuCtD,iBAAeuD,SAASkD,QAAWA,OAAOV,WACtFQ,KAAKG,UAAW"} \ No newline at end of file +{"version":3,"file":"filter.min.js","sources":["../src/filter.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Question bank filter management.\n *\n * @module core_question/filter\n * @copyright 2021 Tomo Tsuyuki \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport CoreFilter from 'core/datafilter';\nimport Notification from 'core/notification';\nimport Selectors from 'core/datafilter/selectors';\nimport Templates from 'core/templates';\nimport Fragment from 'core/fragment';\n\n/**\n * Initialise the question bank filter on the element with the given id.\n *\n * @param {String} filterRegionId ID of the HTML element containing the filters.\n * @param {String} defaultcourseid Course ID for the default course to pass back to the view.\n * @param {String} defaultcategoryid Question bank category ID for the default course to pass back to the view.\n * @param {Number} perpage The number of questions to display per page.\n * @param {Number} contextId Context ID of the question bank view.\n * @param {string} component Frankenstyle name of the component for the fragment API callback (e.g. core_question)\n * @param {string} callback Name of the callback for the fragment API (e.g question_data)\n * @param {string} view The class name of the question bank view class used for this page.\n * @param {Number} cmid If we are in an activitiy, the course module ID.\n * @param {string} pagevars JSON-encoded parameters from passed from the view, including filters and jointype.\n * @param {string} extraparams JSON-encoded additional parameters specific to this view class, used for re-rendering the view.\n */\nexport const init = (\n filterRegionId,\n defaultcourseid,\n defaultcategoryid,\n perpage,\n contextId,\n component,\n callback,\n view,\n cmid,\n pagevars,\n extraparams\n) => {\n\n const SELECTORS = {\n QUESTION_CONTAINER_ID: '#questionscontainer',\n QUESTION_TABLE: '#questionscontainer table',\n SORT_LINK: '#questionscontainer div.sorters a',\n PAGINATION_LINK: '#questionscontainer a[href].page-link',\n LASTCHANGED_FIELD: '#questionsubmit input[name=lastchanged]',\n BULK_ACTIONS: '#bulkactionsui-container input',\n MENU_ACTIONS: '.menu-action',\n EDIT_SWITCH: '.editmode-switch-form input[name=setmode]',\n EDIT_SWITCH_URL: '.editmode-switch-form input[name=pageurl]',\n };\n\n const filterSet = document.querySelector(`#${filterRegionId}`);\n\n const viewData = {\n extraparams,\n cmid,\n view,\n cat: defaultcategoryid,\n courseid: defaultcourseid,\n filter: {},\n jointype: 0,\n qpage: 0,\n qperpage: perpage,\n sortdata: {},\n lastchanged: document.querySelector(SELECTORS.LASTCHANGED_FIELD)?.value ?? null,\n };\n\n let sortData = {};\n const defaultSort = document.querySelector(SELECTORS.QUESTION_TABLE)?.dataset?.defaultsort;\n if (defaultSort) {\n sortData = JSON.parse(defaultSort);\n }\n\n /**\n * Retrieve table data.\n *\n * @param {Object} filterdata data\n * @param {Promise} pendingPromise pending promise\n */\n const applyFilter = (filterdata, pendingPromise) => {\n // Reload the questions based on the specified filters. If no filters are provided,\n // use the default category filter condition.\n if (filterdata) {\n // Main join types.\n viewData.jointype = parseInt(filterSet.dataset.filterverb, 10);\n delete filterdata.jointype;\n // Retrieve filter info.\n viewData.filter = filterdata;\n if (Object.keys(filterdata).length !== 0) {\n if (!isNaN(viewData.jointype)) {\n filterdata.jointype = viewData.jointype;\n }\n updateUrlParams(filterdata);\n }\n }\n // Load questions for first page.\n viewData.filter = JSON.stringify(filterdata);\n viewData.sortdata = JSON.stringify(sortData);\n Fragment.loadFragment(component, callback, contextId, viewData)\n // Render questions for first page and pagination.\n .then((questionhtml, jsfooter) => {\n const questionscontainer = document.querySelector(SELECTORS.QUESTION_CONTAINER_ID);\n if (questionhtml === undefined) {\n questionhtml = '';\n }\n if (jsfooter === undefined) {\n jsfooter = '';\n }\n Templates.replaceNode(questionscontainer, questionhtml, jsfooter);\n // Resolve filter promise.\n if (pendingPromise) {\n pendingPromise.resolve();\n }\n return {questionhtml, jsfooter};\n })\n .catch(Notification.exception);\n };\n\n // Init core filter processor with apply callback.\n const coreFilter = new CoreFilter(filterSet, applyFilter);\n coreFilter.activeFilters = {}; // Unset useless courseid filter.\n coreFilter.init();\n\n /**\n * Update URL Param based upon the current filter.\n *\n * @param {Object} filters Active filters.\n */\n const updateUrlParams = (filters) => {\n const url = new URL(location.href);\n const filterQuery = JSON.stringify(filters);\n url.searchParams.set('filter', filterQuery);\n history.pushState(filters, '', url);\n const editSwitch = document.querySelector(SELECTORS.EDIT_SWITCH);\n if (editSwitch) {\n const editSwitchUrlInput = document.querySelector(SELECTORS.EDIT_SWITCH_URL);\n const editSwitchUrl = new URL(editSwitchUrlInput.value);\n editSwitchUrl.searchParams.set('filter', filterQuery);\n editSwitchUrlInput.value = editSwitchUrl;\n editSwitch.dataset.pageurl = editSwitchUrl;\n }\n };\n\n /**\n * Cleans URL parameters.\n */\n const cleanUrlParams = () => {\n const queryString = location.search;\n const urlParams = new URLSearchParams(queryString);\n if (urlParams.has('cmid')) {\n const cleanedUrl = new URL(location.href.replace(location.search, ''));\n cleanedUrl.searchParams.set('cmid', urlParams.get('cmid'));\n history.pushState({}, '', cleanedUrl);\n }\n\n if (urlParams.has('courseid')) {\n const cleanedUrl = new URL(location.href.replace(location.search, ''));\n cleanedUrl.searchParams.set('courseid', urlParams.get('courseid'));\n history.pushState({}, '', cleanedUrl);\n }\n };\n\n // Add listeners for the sorting, paging and clear actions.\n document.addEventListener('click', e => {\n const sortableLink = e.target.closest(SELECTORS.SORT_LINK);\n const paginationLink = e.target.closest(SELECTORS.PAGINATION_LINK);\n const clearLink = e.target.closest(Selectors.filterset.actions.resetFilters);\n if (sortableLink) {\n e.preventDefault();\n const oldSort = sortData;\n sortData = {};\n sortData[sortableLink.dataset.sortname] = sortableLink.dataset.sortorder;\n for (const sortname in oldSort) {\n if (sortname !== sortableLink.dataset.sortname) {\n sortData[sortname] = oldSort[sortname];\n }\n }\n viewData.qpage = 0;\n coreFilter.updateTableFromFilter();\n }\n if (paginationLink) {\n e.preventDefault();\n const paginationURL = new URL(paginationLink.getAttribute(\"href\"));\n const qpage = paginationURL.searchParams.get('qpage');\n if (paginationURL.search !== null) {\n viewData.qpage = qpage;\n coreFilter.updateTableFromFilter();\n }\n }\n if (clearLink) {\n cleanUrlParams();\n }\n });\n\n // Run apply filter at page load.\n pagevars = JSON.parse(pagevars);\n let initialFilters;\n let jointype = null;\n if (pagevars.filter) {\n // Load initial filter based on page vars.\n initialFilters = pagevars.filter;\n if (pagevars.jointype) {\n jointype = pagevars.jointype;\n }\n }\n\n if (Object.entries(initialFilters).length !== 0) {\n // Remove the default empty filter row.\n const emptyFilterRow = filterSet.querySelector(Selectors.filterset.regions.emptyFilterRow);\n if (emptyFilterRow) {\n emptyFilterRow.remove();\n }\n\n // Add filters.\n let rowcount = 0;\n for (const urlFilter in initialFilters) {\n if (urlFilter === 'jointype') {\n jointype = initialFilters[urlFilter];\n continue;\n }\n // Add each filter row.\n rowcount += 1;\n const filterdata = {\n filtertype: urlFilter,\n values: initialFilters[urlFilter].values,\n jointype: initialFilters[urlFilter].jointype,\n filteroptions: initialFilters[urlFilter].filteroptions,\n rownum: rowcount\n };\n coreFilter.addFilterRow(filterdata);\n }\n coreFilter.filterSet.dataset.filterverb = jointype;\n\n // Since we must filter by category, it does not make sense to allow the top-level \"match any\" or \"match none\" conditions,\n // as this would exclude the category. Remove those options and disable the select.\n const join = coreFilter.filterSet.querySelector(Selectors.filterset.fields.join);\n join.querySelectorAll(`option:not([value=\"${jointype}\"])`).forEach((option) => option.remove());\n join.disabled = true;\n }\n};\n"],"names":["filterRegionId","defaultcourseid","defaultcategoryid","perpage","contextId","component","callback","view","cmid","pagevars","extraparams","SELECTORS","filterSet","document","querySelector","viewData","cat","courseid","filter","jointype","qpage","qperpage","sortdata","lastchanged","_document$querySelect2","value","sortData","defaultSort","_document$querySelect3","dataset","_document$querySelect4","defaultsort","JSON","parse","coreFilter","CoreFilter","filterdata","pendingPromise","parseInt","filterverb","Object","keys","length","isNaN","updateUrlParams","stringify","loadFragment","then","questionhtml","jsfooter","questionscontainer","undefined","replaceNode","resolve","catch","Notification","exception","activeFilters","init","filters","url","URL","location","href","filterQuery","searchParams","set","history","pushState","editSwitch","editSwitchUrlInput","editSwitchUrl","pageurl","initialFilters","addEventListener","e","sortableLink","target","closest","paginationLink","clearLink","Selectors","filterset","actions","resetFilters","preventDefault","oldSort","sortname","sortorder","updateTableFromFilter","paginationURL","getAttribute","get","search","queryString","urlParams","URLSearchParams","has","cleanedUrl","replace","cleanUrlParams","entries","emptyFilterRow","regions","remove","rowcount","urlFilter","filtertype","values","filteroptions","rownum","addFilterRow","join","fields","querySelectorAll","forEach","option","disabled"],"mappings":";;;;;;;4UA4CoB,CAChBA,eACAC,gBACAC,kBACAC,QACAC,UACAC,UACAC,SACAC,KACAC,KACAC,SACAC,oHAGMC,gCACqB,sBADrBA,yBAEc,4BAFdA,oBAGS,oCAHTA,0BAIe,wCAJfA,4BAKiB,0CALjBA,sBAQW,4CARXA,0BASe,4CAGfC,UAAYC,SAASC,yBAAkBd,iBAEvCe,SAAW,CACbL,YAAAA,YACAF,KAAAA,KACAD,KAAAA,KACAS,IAAKd,kBACLe,SAAUhB,gBACViB,OAAQ,GACRC,SAAU,EACVC,MAAO,EACPC,SAAUlB,QACVmB,SAAU,GACVC,yEAAaV,SAASC,cAAcH,sEAAvBa,uBAAqDC,6DAAS,UAG3EC,SAAW,SACTC,2CAAcd,SAASC,cAAcH,4FAAvBiB,uBAAkDC,iDAAlDC,uBAA2DC,YAC3EJ,cACAD,SAAWM,KAAKC,MAAMN,oBAiDpBO,WAAa,IAAIC,oBAAWvB,WAxCd,CAACwB,WAAYC,kBAGzBD,aAEArB,SAASI,SAAWmB,SAAS1B,UAAUiB,QAAQU,WAAY,WACpDH,WAAWjB,SAElBJ,SAASG,OAASkB,WACqB,IAAnCI,OAAOC,KAAKL,YAAYM,SACnBC,MAAM5B,SAASI,YAChBiB,WAAWjB,SAAWJ,SAASI,UAEnCyB,gBAAgBR,cAIxBrB,SAASG,OAASc,KAAKa,UAAUT,YACjCrB,SAASO,SAAWU,KAAKa,UAAUnB,4BAC1BoB,aAAazC,UAAWC,SAAUF,UAAWW,UAEjDgC,MAAK,CAACC,aAAcC,kBACXC,mBAAqBrC,SAASC,cAAcH,6CAC7BwC,IAAjBH,eACAA,aAAe,SAEFG,IAAbF,WACAA,SAAW,uBAELG,YAAYF,mBAAoBF,aAAcC,UAEpDZ,gBACAA,eAAegB,UAEZ,CAACL,aAAAA,aAAcC,SAAAA,aAEzBK,MAAMC,sBAAaC,cAK5BtB,WAAWuB,cAAgB,GAC3BvB,WAAWwB,aAOLd,gBAAmBe,gBACfC,IAAM,IAAIC,IAAIC,SAASC,MACvBC,YAAchC,KAAKa,UAAUc,SACnCC,IAAIK,aAAaC,IAAI,SAAUF,aAC/BG,QAAQC,UAAUT,QAAS,GAAIC,WACzBS,WAAaxD,SAASC,cAAcH,0BACtC0D,WAAY,OACNC,mBAAqBzD,SAASC,cAAcH,2BAC5C4D,cAAgB,IAAIV,IAAIS,mBAAmB7C,OACjD8C,cAAcN,aAAaC,IAAI,SAAUF,aACzCM,mBAAmB7C,MAAQ8C,cAC3BF,WAAWxC,QAAQ2C,QAAUD,oBAyDjCE,eAjCJ5D,SAAS6D,iBAAiB,SAASC,UACzBC,aAAeD,EAAEE,OAAOC,QAAQnE,qBAChCoE,eAAiBJ,EAAEE,OAAOC,QAAQnE,2BAClCqE,UAAYL,EAAEE,OAAOC,QAAQG,mBAAUC,UAAUC,QAAQC,iBAC3DR,aAAc,CACdD,EAAEU,uBACIC,QAAU5D,SAChBA,SAAW,GACXA,SAASkD,aAAa/C,QAAQ0D,UAAYX,aAAa/C,QAAQ2D,cAC1D,MAAMD,YAAYD,QACfC,WAAaX,aAAa/C,QAAQ0D,WAClC7D,SAAS6D,UAAYD,QAAQC,WAGrCxE,SAASK,MAAQ,EACjBc,WAAWuD,2BAEXV,eAAgB,CAChBJ,EAAEU,uBACIK,cAAgB,IAAI7B,IAAIkB,eAAeY,aAAa,SACpDvE,MAAQsE,cAAczB,aAAa2B,IAAI,SAChB,OAAzBF,cAAcG,SACd9E,SAASK,MAAQA,MACjBc,WAAWuD,yBAGfT,WA3Ce,YACbc,YAAchC,SAAS+B,OACvBE,UAAY,IAAIC,gBAAgBF,gBAClCC,UAAUE,IAAI,QAAS,OACjBC,WAAa,IAAIrC,IAAIC,SAASC,KAAKoC,QAAQrC,SAAS+B,OAAQ,KAClEK,WAAWjC,aAAaC,IAAI,OAAQ6B,UAAUH,IAAI,SAClDzB,QAAQC,UAAU,GAAI,GAAI8B,eAG1BH,UAAUE,IAAI,YAAa,OACrBC,WAAa,IAAIrC,IAAIC,SAASC,KAAKoC,QAAQrC,SAAS+B,OAAQ,KAClEK,WAAWjC,aAAaC,IAAI,WAAY6B,UAAUH,IAAI,aACtDzB,QAAQC,UAAU,GAAI,GAAI8B,cAgC1BE,UAOJjF,SAAW,SAFfV,SAAWuB,KAAKC,MAAMxB,WAGTS,SAETuD,eAAiBhE,SAASS,OACtBT,SAASU,WACTA,SAAWV,SAASU,WAIkB,IAA1CqB,OAAO6D,QAAQ5B,gBAAgB/B,OAAc,OAEvC4D,eAAiB1F,UAAUE,cAAcmE,mBAAUC,UAAUqB,QAAQD,gBACvEA,gBACAA,eAAeE,aAIfC,SAAW,MACV,MAAMC,aAAajC,eAAgB,IAClB,aAAdiC,UAA0B,CAC1BvF,SAAWsD,eAAeiC,oBAI9BD,UAAY,QACNrE,WAAa,CACfuE,WAAYD,UACZE,OAASnC,eAAeiC,WAAWE,OACnCzF,SAAUsD,eAAeiC,WAAWvF,SACpC0F,cAAepC,eAAeiC,WAAWG,cACzCC,OAAQL,UAEZvE,WAAW6E,aAAa3E,YAE5BF,WAAWtB,UAAUiB,QAAQU,WAAapB,eAIpC6F,KAAO9E,WAAWtB,UAAUE,cAAcmE,mBAAUC,UAAU+B,OAAOD,MAC3EA,KAAKE,8CAAuC/F,iBAAegG,SAASC,QAAWA,OAAOZ,WACtFQ,KAAKK,UAAW"} \ No newline at end of file diff --git a/question/amd/build/refresh_ui.min.js b/question/amd/build/refresh_ui.min.js new file mode 100644 index 00000000000..14fe30fe992 --- /dev/null +++ b/question/amd/build/refresh_ui.min.js @@ -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 \ No newline at end of file diff --git a/question/amd/build/refresh_ui.min.js.map b/question/amd/build/refresh_ui.min.js.map new file mode 100644 index 00000000000..3a5f92fddae --- /dev/null +++ b/question/amd/build/refresh_ui.min.js.map @@ -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 .\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"} \ No newline at end of file diff --git a/question/amd/src/filter.js b/question/amd/src/filter.js index 5cdcb841929..6e9487e8acc 100644 --- a/question/amd/src/filter.js +++ b/question/amd/src/filter.js @@ -70,7 +70,10 @@ export const init = ( const filterSet = document.querySelector(`#${filterRegionId}`); - const filterCondition = { + const viewData = { + extraparams, + cmid, + view, cat: defaultcategoryid, courseid: defaultcourseid, filter: {}, @@ -78,16 +81,15 @@ export const init = ( qpage: 0, qperpage: perpage, sortdata: {}, - tabname: 'questions', + lastchanged: document.querySelector(SELECTORS.LASTCHANGED_FIELD)?.value ?? null, }; + let sortData = {}; const defaultSort = document.querySelector(SELECTORS.QUESTION_TABLE)?.dataset?.defaultsort; if (defaultSort) { - filterCondition.sortData = JSON.parse(defaultSort); + sortData = JSON.parse(defaultSort); } - let filterQuery = ''; - /** * Retrieve table data. * @@ -99,26 +101,20 @@ export const init = ( // use the default category filter condition. if (filterdata) { // Main join types. - filterCondition.jointype = parseInt(filterSet.dataset.filterverb, 10); + viewData.jointype = parseInt(filterSet.dataset.filterverb, 10); delete filterdata.jointype; // Retrieve filter info. - filterCondition.filter = filterdata; + viewData.filter = filterdata; if (Object.keys(filterdata).length !== 0) { - if (!isNaN(filterCondition.jointype)) { - filterdata.jointype = filterCondition.jointype; + if (!isNaN(viewData.jointype)) { + filterdata.jointype = viewData.jointype; } updateUrlParams(filterdata); } } // Load questions for first page. - const viewData = { - view: view, - cmid: cmid, - filtercondition: JSON.stringify(filterCondition), - extraparams: extraparams, - filterquery: filterQuery, - lastchanged: document.querySelector(SELECTORS.LASTCHANGED_FIELD)?.value ?? null - }; + 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) => { @@ -129,7 +125,7 @@ export const init = ( if (jsfooter === undefined) { jsfooter = ''; } - Templates.replaceNodeContents(questionscontainer, questionhtml, jsfooter); + Templates.replaceNode(questionscontainer, questionhtml, jsfooter); // Resolve filter promise. if (pendingPromise) { pendingPromise.resolve(); @@ -154,13 +150,6 @@ export const init = ( const filterQuery = JSON.stringify(filters); url.searchParams.set('filter', filterQuery); history.pushState(filters, '', url); - document.querySelectorAll(SELECTORS.BULK_ACTIONS).forEach(bulkAction => { - const actionUrl = new URL(bulkAction.formAction); - const returnUrl = new URL(actionUrl.searchParams.get('returnurl')); - returnUrl.searchParams.set('filter', filterQuery); - actionUrl.searchParams.set('returnurl', returnUrl); - bulkAction.formAction = actionUrl; - }); const editSwitch = document.querySelector(SELECTORS.EDIT_SWITCH); if (editSwitch) { const editSwitchUrlInput = document.querySelector(SELECTORS.EDIT_SWITCH_URL); @@ -197,15 +186,15 @@ export const init = ( const clearLink = e.target.closest(Selectors.filterset.actions.resetFilters); if (sortableLink) { e.preventDefault(); - const oldSort = filterCondition.sortdata; - filterCondition.sortdata = {}; - filterCondition.sortdata[sortableLink.dataset.sortname] = sortableLink.dataset.sortorder; + const oldSort = sortData; + sortData = {}; + sortData[sortableLink.dataset.sortname] = sortableLink.dataset.sortorder; for (const sortname in oldSort) { if (sortname !== sortableLink.dataset.sortname) { - filterCondition.sortdata[sortname] = oldSort[sortname]; + sortData[sortname] = oldSort[sortname]; } } - filterCondition.qpage = 0; + viewData.qpage = 0; coreFilter.updateTableFromFilter(); } if (paginationLink) { @@ -213,7 +202,7 @@ export const init = ( const paginationURL = new URL(paginationLink.getAttribute("href")); const qpage = paginationURL.searchParams.get('qpage'); if (paginationURL.search !== null) { - filterCondition.qpage = qpage; + viewData.qpage = qpage; coreFilter.updateTableFromFilter(); } } diff --git a/question/amd/src/refresh_ui.js b/question/amd/src/refresh_ui.js new file mode 100644 index 00000000000..1b0708802e4 --- /dev/null +++ b/question/amd/src/refresh_ui.js @@ -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 . + +/** + * 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); + }); + } +}; diff --git a/question/bank/columnsortorder/actions.php b/question/bank/columnsortorder/actions.php new file mode 100644 index 00000000000..56d18c65124 --- /dev/null +++ b/question/bank/columnsortorder/actions.php @@ -0,0 +1,98 @@ +. + +/** + * 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 + * @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)); diff --git a/question/bank/columnsortorder/amd/build/actions.min.js b/question/bank/columnsortorder/amd/build/actions.min.js new file mode 100644 index 00000000000..6b786ef2d30 --- /dev/null +++ b/question/bank/columnsortorder/amd/build/actions.min.js @@ -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 + * @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("&","&"));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 \ No newline at end of file diff --git a/question/bank/columnsortorder/amd/build/actions.min.js.map b/question/bank/columnsortorder/amd/build/actions.min.js.map new file mode 100644 index 00000000000..fd2b72afe66 --- /dev/null +++ b/question/bank/columnsortorder/amd/build/actions.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"actions.min.js","sources":["../src/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 .\n\n/**\n * Common javascript for handling actions on the admin page and the user's view of the question bank.\n *\n * @module qbank_columnsortorder/actions\n * @copyright 2023 onwards Catalyst IT Europe Ltd\n * @author Mark Johnson \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport SortableList from 'core/sortable_list';\nimport $ from 'jquery';\nimport * as repository from 'qbank_columnsortorder/repository';\nimport Notification from \"core/notification\";\nimport RefreshUi from 'core_question/refresh_ui';\n\nexport const SELECTORS = {\n columnList: '.qbank-column-list',\n sortableColumn: '.qbank-sortable-column',\n removeLink: '[data-action=remove]',\n moveHandler: '[data-drag-type=move]',\n addColumn: '.addcolumn',\n addLink: '[data-action=add]',\n actionLink: '.action-link',\n};\n\n/**\n * Sets up sortable list in the column sort order page.\n *\n * @param {Element} listRoot Element containing the sortable list.\n * @param {Boolean} vertical Is the list in vertical orientation, rather than horizonal?\n * @param {Boolean} global Should changes be saved to global config, rather than user preferences?\n * @return {jQuery} sortable column elements, for attaching additional event listeners.\n */\nexport const setupSortableLists = (listRoot, vertical = false, global = false) => {\n const sortableList = new SortableList(listRoot, {\n moveHandlerSelector: SELECTORS.moveHandler,\n isHorizontal: !vertical,\n });\n sortableList.getElementName = element => Promise.resolve(element.data('name'));\n\n const sortableColumns = $(SELECTORS.sortableColumn);\n\n sortableColumns.on(SortableList.EVENTS.DROP, () => {\n repository.setColumnbankOrder(getColumnOrder(listRoot), global).catch(Notification.exception);\n listRoot.querySelectorAll(SELECTORS.sortableColumn).forEach(item => item.classList.remove('active'));\n });\n\n sortableColumns.on(SortableList.EVENTS.DRAGSTART, (event) => {\n event.currentTarget.classList.add('active');\n });\n\n return sortableColumns;\n};\n\n/**\n * Set up event handlers for action buttons.\n *\n * For each action, call the web service to update the appropriate setting or user preference, then call the fragment to\n * refresh the view.\n *\n * @param {Element} uiRoot The root of the question bank UI.\n * @param {Boolean} global Should changes be saved to global config, rather than user preferences?\n */\nexport const setupActionButtons = (uiRoot, global = false) => {\n uiRoot.addEventListener('click', async(e) => {\n const actionLink = e.target.closest(SELECTORS.actionLink);\n if (!actionLink) {\n return;\n }\n try {\n e.preventDefault();\n const action = actionLink.dataset.action;\n if (action === 'add' || action === 'remove') {\n const hiddenColumns = [];\n const addColumnList = document.querySelector(SELECTORS.addColumn);\n if (addColumnList) {\n addColumnList.querySelectorAll(SELECTORS.addLink).forEach(item => {\n if (action === 'add' && item === actionLink) {\n return;\n }\n hiddenColumns.push(item.dataset.column);\n });\n }\n if (action === 'remove') {\n hiddenColumns.push(actionLink.dataset.column);\n }\n await repository.setHiddenColumns(hiddenColumns, global);\n } else if (action === 'reset') {\n await repository.resetColumns(global);\n }\n const actionUrl = new URL(actionLink.href);\n const returnUrl = new URL(actionUrl.searchParams.get('returnurl').replaceAll('&', '&'));\n await RefreshUi.refresh(uiRoot, returnUrl);\n } catch (ex) {\n await Notification.exception(ex);\n }\n });\n};\n\n/**\n * Gets the newly reordered columns to display in the question bank view.\n * @param {Element} listRoot\n * @returns {Array}\n */\nexport const getColumnOrder = listRoot => {\n const columns = Array.from(listRoot.querySelectorAll('[data-columnid]'))\n .map(column => column.dataset.columnid);\n\n return columns.filter((value, index) => columns.indexOf(value) === index);\n};\n"],"names":["SELECTORS","columnList","sortableColumn","removeLink","moveHandler","addColumn","addLink","actionLink","listRoot","vertical","global","sortableList","SortableList","moveHandlerSelector","isHorizontal","getElementName","element","Promise","resolve","data","sortableColumns","on","EVENTS","DROP","repository","setColumnbankOrder","getColumnOrder","catch","Notification","exception","querySelectorAll","forEach","item","classList","remove","DRAGSTART","event","currentTarget","add","uiRoot","addEventListener","async","e","target","closest","preventDefault","action","dataset","hiddenColumns","addColumnList","document","querySelector","push","column","setHiddenColumns","resetColumns","actionUrl","URL","href","returnUrl","searchParams","get","replaceAll","RefreshUi","refresh","ex","columns","Array","from","map","columnid","filter","value","index","indexOf"],"mappings":";;;;;;;;mhCA8BaA,UAAY,CACrBC,WAAY,qBACZC,eAAgB,yBAChBC,WAAY,uBACZC,YAAa,wBACbC,UAAW,aACXC,QAAS,oBACTC,WAAY,yEAWkB,SAACC,cAAUC,iEAAkBC,qEACrDC,aAAe,IAAIC,uBAAaJ,SAAU,CAC5CK,oBAAqBb,UAAUI,YAC/BU,cAAeL,WAEnBE,aAAaI,eAAiBC,SAAWC,QAAQC,QAAQF,QAAQG,KAAK,eAEhEC,iBAAkB,mBAAEpB,UAAUE,uBAEpCkB,gBAAgBC,GAAGT,uBAAaU,OAAOC,MAAM,KACzCC,WAAWC,mBAAmBC,eAAelB,UAAWE,QAAQiB,MAAMC,sBAAaC,WACnFrB,SAASsB,iBAAiB9B,UAAUE,gBAAgB6B,SAAQC,MAAQA,KAAKC,UAAUC,OAAO,eAG9Fd,gBAAgBC,GAAGT,uBAAaU,OAAOa,WAAYC,QAC/CA,MAAMC,cAAcJ,UAAUK,IAAI,aAG/BlB,6CAYuB,SAACmB,YAAQ7B,+DACvC6B,OAAOC,iBAAiB,SAASC,MAAAA,UACvBlC,WAAamC,EAAEC,OAAOC,QAAQ5C,UAAUO,eACzCA,eAIDmC,EAAEG,uBACIC,OAASvC,WAAWwC,QAAQD,UACnB,QAAXA,QAA+B,WAAXA,OAAqB,OACnCE,cAAgB,GAChBC,cAAgBC,SAASC,cAAcnD,UAAUK,WACnD4C,eACAA,cAAcnB,iBAAiB9B,UAAUM,SAASyB,SAAQC,OACvC,QAAXc,QAAoBd,OAASzB,YAGjCyC,cAAcI,KAAKpB,KAAKe,QAAQM,WAGzB,WAAXP,QACAE,cAAcI,KAAK7C,WAAWwC,QAAQM,cAEpC7B,WAAW8B,iBAAiBN,cAAetC,YAC/B,UAAXoC,cACDtB,WAAW+B,aAAa7C,cAE5B8C,UAAY,IAAIC,IAAIlD,WAAWmD,MAC/BC,UAAY,IAAIF,IAAID,UAAUI,aAAaC,IAAI,aAAaC,WAAW,QAAS,YAChFC,oBAAUC,QAAQzB,OAAQoB,WAClC,MAAOM,UACCrC,sBAAaC,UAAUoC,eAU5BvC,eAAiBlB,iBACpB0D,QAAUC,MAAMC,KAAK5D,SAASsB,iBAAiB,oBAChDuC,KAAIhB,QAAUA,OAAON,QAAQuB,kBAE3BJ,QAAQK,QAAO,CAACC,MAAOC,QAAUP,QAAQQ,QAAQF,SAAWC"} \ No newline at end of file diff --git a/question/bank/columnsortorder/amd/build/admin_actions.min.js b/question/bank/columnsortorder/amd/build/admin_actions.min.js new file mode 100644 index 00000000000..5754b27c2c0 --- /dev/null +++ b/question/bank/columnsortorder/amd/build/admin_actions.min.js @@ -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 + * @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 \ No newline at end of file diff --git a/question/bank/columnsortorder/amd/build/admin_actions.min.js.map b/question/bank/columnsortorder/amd/build/admin_actions.min.js.map new file mode 100644 index 00000000000..1576e7991cc --- /dev/null +++ b/question/bank/columnsortorder/amd/build/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 .\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 \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"} \ No newline at end of file diff --git a/question/bank/columnsortorder/amd/build/repository.min.js b/question/bank/columnsortorder/amd/build/repository.min.js new file mode 100644 index 00000000000..f4078cc90c5 --- /dev/null +++ b/question/bank/columnsortorder/amd/build/repository.min.js @@ -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 \ No newline at end of file diff --git a/question/bank/columnsortorder/amd/build/repository.min.js.map b/question/bank/columnsortorder/amd/build/repository.min.js.map new file mode 100644 index 00000000000..c8412e93976 --- /dev/null +++ b/question/bank/columnsortorder/amd/build/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 .\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 \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"} \ No newline at end of file diff --git a/question/bank/columnsortorder/amd/build/sort_columns.min.js b/question/bank/columnsortorder/amd/build/sort_columns.min.js deleted file mode 100644 index 37d3a74bcb6..00000000000 --- a/question/bank/columnsortorder/amd/build/sort_columns.min.js +++ /dev/null @@ -1,10 +0,0 @@ -define("qbank_columnsortorder/sort_columns",["exports","core/ajax","core/notification","core/sortable_list","jquery"],(function(_exports,_ajax,_notification,_sortable_list,_jquery){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} -/** - * Javascript for sorting columns in question bank view. - * - * @copyright 2021 Catalyst IT Australia Pty Ltd - * @author Ghaly Marc-Alexandre - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_sortable_list=_interopRequireDefault(_sortable_list),_jquery=_interopRequireDefault(_jquery);const setOrder=columns=>(0,_ajax.call)([{methodname:"qbank_columnsortorder_set_columnbank_order",args:{columns:columns}}])[0],getColumnOrder=listRoot=>{const columns=Array.from(listRoot.querySelectorAll("[data-pluginname]")).map((column=>column.dataset.pluginname));return columns.filter(((value,index)=>columns.indexOf(value)===index))};_exports.init=id=>{(listRoot=>{new _sortable_list.default(".list",{moveHandlerSelector:".item"}),(0,_jquery.default)(".item").on(_sortable_list.default.EVENTS.DROP,(()=>{const columns=getColumnOrder(listRoot);setOrder(columns).catch(_notification.exception),listRoot.querySelectorAll(".item").forEach((item=>item.classList.remove("active")))})),(0,_jquery.default)(".item").on(_sortable_list.default.EVENTS.DRAGSTART,(event=>{event.currentTarget.classList.add("active")}))})(document.querySelector("#".concat(id)))}})); - -//# sourceMappingURL=sort_columns.min.js.map \ No newline at end of file diff --git a/question/bank/columnsortorder/amd/build/sort_columns.min.js.map b/question/bank/columnsortorder/amd/build/sort_columns.min.js.map deleted file mode 100644 index 998848cbbc7..00000000000 --- a/question/bank/columnsortorder/amd/build/sort_columns.min.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"sort_columns.min.js","sources":["../src/sort_columns.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Javascript for sorting columns in question bank view.\n *\n * @copyright 2021 Catalyst IT Australia Pty Ltd\n * @author Ghaly Marc-Alexandre \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {call as fetchMany} from 'core/ajax';\nimport {exception as displayException} from 'core/notification';\nimport SortableList from 'core/sortable_list';\nimport jQuery from 'jquery';\n\n/**\n * Sets up sortable list in the column sort order page.\n * @param {Element} listRoot\n */\nconst setupSortableLists = (listRoot) => {\n new SortableList('.list', {\n moveHandlerSelector: '.item',\n });\n\n jQuery('.item').on(SortableList.EVENTS.DROP, () => {\n const columns = getColumnOrder(listRoot);\n setOrder(columns).catch(displayException);\n listRoot.querySelectorAll('.item').forEach(item => item.classList.remove('active'));\n });\n\n jQuery('.item').on(SortableList.EVENTS.DRAGSTART, (event) => {\n event.currentTarget.classList.add('active');\n });\n};\n\n/**\n * Call external function set_order - inserts the updated column in the config_plugins table.\n *\n * @param {String} columns String that contains column order.\n * @returns {Promise}\n */\nconst setOrder = columns => fetchMany([{\n methodname: 'qbank_columnsortorder_set_columnbank_order',\n args: {columns},\n}])[0];\n\n/**\n * Gets the newly reordered columns to display in the question bank view.\n * @param {Element} listRoot\n * @returns {Array}\n */\nconst getColumnOrder = listRoot => {\n const columns = Array.from(listRoot.querySelectorAll('[data-pluginname]'))\n .map(column => column.dataset.pluginname);\n\n return columns.filter((value, index) => columns.indexOf(value) === index);\n};\n\n/**\n * Initialize module\n * @param {String} id unique id for columns.\n */\nexport const init = id => {\n const listRoot = document.querySelector(`#${id}`);\n setupSortableLists(listRoot);\n};\n"],"names":["setOrder","columns","methodname","args","getColumnOrder","listRoot","Array","from","querySelectorAll","map","column","dataset","pluginname","filter","value","index","indexOf","id","SortableList","moveHandlerSelector","on","EVENTS","DROP","catch","displayException","forEach","item","classList","remove","DRAGSTART","event","currentTarget","add","setupSortableLists","document","querySelector"],"mappings":";;;;;;;sLAsDMA,SAAWC,UAAW,cAAU,CAAC,CACnCC,WAAY,6CACZC,KAAM,CAACF,QAAAA,YACP,GAOEG,eAAiBC,iBACbJ,QAAUK,MAAMC,KAAKF,SAASG,iBAAiB,sBAChDC,KAAIC,QAAUA,OAAOC,QAAQC,oBAE3BX,QAAQY,QAAO,CAACC,MAAOC,QAAUd,QAAQe,QAAQF,SAAWC,uBAOnDE,KA3CQZ,CAAAA,eACpBa,uBAAa,QAAS,CACtBC,oBAAqB,8BAGlB,SAASC,GAAGF,uBAAaG,OAAOC,MAAM,WACnCrB,QAAUG,eAAeC,UAC/BL,SAASC,SAASsB,MAAMC,yBACxBnB,SAASG,iBAAiB,SAASiB,SAAQC,MAAQA,KAAKC,UAAUC,OAAO,mCAGtE,SAASR,GAAGF,uBAAaG,OAAOQ,WAAYC,QAC/CA,MAAMC,cAAcJ,UAAUK,IAAI,cAiCtCC,CADiBC,SAASC,yBAAkBlB"} \ No newline at end of file diff --git a/question/bank/columnsortorder/amd/build/user_actions.min.js b/question/bank/columnsortorder/amd/build/user_actions.min.js new file mode 100644 index 00000000000..4228e27cc9f --- /dev/null +++ b/question/bank/columnsortorder/amd/build/user_actions.min.js @@ -0,0 +1,11 @@ +define("qbank_columnsortorder/user_actions",["exports","qbank_columnsortorder/actions","qbank_columnsortorder/repository","core/str","core/modal_events","core/modal_save_cancel","core/notification","core/sortable_list","core/templates"],(function(_exports,actions,repository,_str,_modal_events,_modal_save_cancel,_notification,_sortable_list,_templates){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 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 + * @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),_modal_events=_interopRequireDefault(_modal_events),_modal_save_cancel=_interopRequireDefault(_modal_save_cancel),_notification=_interopRequireDefault(_notification),_sortable_list=_interopRequireDefault(_sortable_list),_templates=_interopRequireDefault(_templates);const SELECTORS_uiRoot=".questionbankwindow",SELECTORS_moveAction=".menu-action[data-action=move]",SELECTORS_resizeAction=".menu-action[data-action=resize]",SELECTORS_resizeHandle=".qbank_columnsortorder-action-handle.resize",SELECTORS_handleContainer=".handle-container",SELECTORS_headerContainer=".header-container",SELECTORS_tableColumn=identifier=>'td[data-columnid="'.concat(identifier.replace(/["\\]/g,"\\$&"),'"]');let currentHeader,currentX,suspendShowHideHandles=!1;const serialiseColumnSizes=uiRoot=>{const columnSizes=[];return uiRoot.querySelectorAll("th").forEach((header=>{const width=parseInt(header.style.width);width&&!isNaN(width)&&columnSizes.push({column:header.dataset.columnid,width:width})})),JSON.stringify(columnSizes)},showResizeModal=async(currentHeader,uiRoot)=>{const initialWidth=currentHeader.offsetWidth,modal=await _modal_save_cancel.default.create({title:(0,_str.get_string)("resizecolumn","qbank_columnsortorder",currentHeader.textContent),body:_templates.default.render("qbank_columnsortorder/resize_modal",{}),show:!0}),root=modal.getRoot();root.on(_modal_events.default.cancel,(()=>{currentHeader.style.width="".concat(initialWidth,"px")})),root.on(_modal_events.default.save,(()=>{repository.setColumnSize(serialiseColumnSizes(uiRoot)).catch(_notification.default.exception)}));const input=(await modal.bodyPromise).get(0).querySelector("input");input.value=initialWidth,input.addEventListener("change",(e=>{const newWidth=e.target.value;currentHeader.style.width="".concat(newWidth,"px")}))},reorderColumns=event=>{const header=event.target,insertAfter=header.previousElementSibling;document.querySelector(SELECTORS_uiRoot).querySelectorAll(SELECTORS_tableColumn(header.dataset.columnid)).forEach((column=>{const row=column.parentElement;if(insertAfter){row.querySelector(SELECTORS_tableColumn(insertAfter.dataset.columnid)).after(column)}else row.insertBefore(column,row.firstChild)}))};_exports.init=async()=>{const uiRoot=document.getElementById("questionscontainer");await(uiRoot=>new Promise((resolve=>{const headerContainers=uiRoot.querySelectorAll(SELECTORS_headerContainer);_templates.default.renderForPromise("qbank_columnsortorder/handle_container",{}).then((_ref=>{let{html:html,js:js}=_ref;return headerContainers.forEach((container=>{_templates.default.prependNodeContents(container,html,js)})),resolve(),headerContainers})).catch(_notification.default.exception)})))(uiRoot),uiRoot.querySelectorAll(SELECTORS_moveAction).forEach((moveAction=>{const header=moveAction.closest("th");header.classList.add("qbank-sortable-column");const handleContainer=header.querySelector(SELECTORS_handleContainer),context={action:"move",dragtype:"move",target:"",title:moveAction.title,pixicon:"i/dragdrop",pixcomponent:"core",popup:!0};return _templates.default.renderForPromise("qbank_columnsortorder/action_handle",context).then((_ref2=>{let{html:html,js:js}=_ref2;return _templates.default.prependNodeContents(handleContainer,html,js),handleContainer})).catch(_notification.default.exception)})),(uiRoot=>{uiRoot.querySelectorAll(SELECTORS_resizeAction).forEach((resizeAction=>{const handleContainer=resizeAction.closest(SELECTORS_headerContainer).querySelector(SELECTORS_handleContainer),context={action:"resize",target:"",title:resizeAction.title,pixicon:"i/twoway",pixcomponent:"core",popup:!0};return _templates.default.renderForPromise("qbank_columnsortorder/action_handle",context).then((_ref3=>{let{html:html,js:js}=_ref3;return _templates.default.appendNodeContents(handleContainer,html,js),handleContainer})).catch(_notification.default.exception)}));let moveTracker=!1,currentResizeHandle=null;uiRoot.addEventListener("mousedown",(e=>{currentResizeHandle=e.target.closest(SELECTORS_resizeHandle),currentResizeHandle&&(currentX=e.pageX,currentHeader=e.target.closest(actions.SELECTORS.sortableColumn),moveTracker=!1,suspendShowHideHandles=!0)})),document.addEventListener("mousemove",(e=>{if(!currentHeader||!currentResizeHandle||0===currentX)return;document.getSelection().removeAllRanges();const offset=e.pageX-currentX;currentX=e.pageX;const newWidth=currentHeader.offsetWidth+offset;currentHeader.style.width=newWidth+"px",moveTracker=!0})),document.addEventListener("mouseup",(()=>{currentHeader&¤tResizeHandle&&0!==currentX&&(moveTracker?repository.setColumnSize(serialiseColumnSizes(uiRoot)).catch(_notification.default.exception):showResizeModal(currentHeader,uiRoot),currentHeader=null,currentResizeHandle=null,currentX=0,moveTracker=!1,suspendShowHideHandles=!1)}))})(uiRoot),(uiRoot=>{let shownHeader=null,tableHead=uiRoot.querySelector("thead");uiRoot.addEventListener("mouseover",(e=>{if(suspendShowHideHandles)return;const header=e.target.closest(actions.SELECTORS.sortableColumn);var _tableHead$querySelec;(header||shownHeader)&&(header&&header===shownHeader||(null===(_tableHead$querySelec=tableHead.querySelector(".show-handles"))||void 0===_tableHead$querySelec||_tableHead$querySelec.classList.remove("show-handles"),shownHeader=header,header&&header.classList.add("show-handles")))}))})(uiRoot);const sortableColumns=actions.setupSortableLists(uiRoot.querySelector(actions.SELECTORS.columnList));sortableColumns.on(_sortable_list.default.EVENTS.DROP,reorderColumns),sortableColumns.on(_sortable_list.default.EVENTS.DRAGSTART,(()=>{suspendShowHideHandles=!0})),sortableColumns.on(_sortable_list.default.EVENTS.DRAGEND,(()=>{suspendShowHideHandles=!1})),(uiRoot=>{uiRoot.addEventListener("click",(e=>{const moveAction=e.target.closest(SELECTORS_moveAction);moveAction&&(e.preventDefault(),moveAction.closest(actions.SELECTORS.sortableColumn).querySelector(actions.SELECTORS.moveHandler).click())}))})(uiRoot),(uiRoot=>{uiRoot.addEventListener("click",(e=>{const resizeAction=e.target.closest(SELECTORS_resizeAction);if(resizeAction){e.preventDefault();const currentHeader=resizeAction.closest("th");showResizeModal(currentHeader,uiRoot)}}))})(uiRoot),actions.setupActionButtons(uiRoot)}})); + +//# sourceMappingURL=user_actions.min.js.map \ No newline at end of file diff --git a/question/bank/columnsortorder/amd/build/user_actions.min.js.map b/question/bank/columnsortorder/amd/build/user_actions.min.js.map new file mode 100644 index 00000000000..322d868ff7b --- /dev/null +++ b/question/bank/columnsortorder/amd/build/user_actions.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"user_actions.min.js","sources":["../src/user_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 .\n\n/**\n * Javascript for customising the user's view of the question bank\n *\n * @module qbank_columnsortorder/user_actions\n * @copyright 2021 Catalyst IT Australia Pty Ltd\n * @author Ghaly Marc-Alexandre \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 {get_string as getString} from 'core/str';\nimport ModalEvents from 'core/modal_events';\nimport ModalSaveCancel from 'core/modal_save_cancel';\nimport Notification from \"core/notification\";\nimport SortableList from 'core/sortable_list';\nimport Templates from \"core/templates\";\n\n\nconst SELECTORS = {\n uiRoot: '.questionbankwindow',\n moveAction: '.menu-action[data-action=move]',\n resizeAction: '.menu-action[data-action=resize]',\n resizeHandle: '.qbank_columnsortorder-action-handle.resize',\n handleContainer: '.handle-container',\n headerContainer: '.header-container',\n tableColumn: identifier => `td[data-columnid=\"${identifier.replace(/[\"\\\\]/g, '\\\\$&')}\"]`,\n};\n\n/** To track mouse event on a table header */\nlet currentHeader;\n\n/** Current mouse x postion, to track mouse event on a table header */\nlet currentX;\n\n/**\n * Flag to temporarily prevent move and resize handles from being shown or hidden.\n *\n * @type {boolean}\n */\nlet suspendShowHideHandles = false;\n\n/**\n * Add handle containers for move and resize handles.\n *\n * @param {Element} uiRoot The root element of the quesiton bank UI.\n * @return {Promise} Resolved after the containers have been added to each column header.\n */\nconst addHandleContainers = uiRoot => {\n return new Promise((resolve) => {\n const headerContainers = uiRoot.querySelectorAll(SELECTORS.headerContainer);\n Templates.renderForPromise('qbank_columnsortorder/handle_container', {})\n .then(({html, js}) => {\n headerContainers.forEach(container => {\n Templates.prependNodeContents(container, html, js);\n });\n resolve();\n return headerContainers;\n }).catch(Notification.exception);\n });\n};\n\n/**\n * Render move handles in each container.\n *\n * This takes a list of the move actions rendered in each column header, and creates a corresponding drag handle for each.\n *\n * @param {NodeList} moveActions Menu actions for moving columns.\n */\nconst setUpMoveHandles = moveActions => {\n moveActions.forEach(moveAction => {\n const header = moveAction.closest('th');\n header.classList.add('qbank-sortable-column');\n const handleContainer = header.querySelector(SELECTORS.handleContainer);\n const context = {\n action: \"move\",\n dragtype: \"move\",\n target: '',\n title: moveAction.title,\n pixicon: \"i/dragdrop\",\n pixcomponent: \"core\",\n popup: true\n };\n return Templates.renderForPromise('qbank_columnsortorder/action_handle', context)\n .then(({html, js}) => {\n Templates.prependNodeContents(handleContainer, html, js);\n return handleContainer;\n }).catch(Notification.exception);\n });\n};\n\n/**\n * Serialise the current column sizes.\n *\n * This finds the current width set in each column header's style property, and returns them encoded as a JSON string.\n *\n * @param {Element} uiRoot The root element of the quesiton bank UI.\n * @return {String} JSON array containing a list of objects with column and width properties.\n */\nconst serialiseColumnSizes = (uiRoot) => {\n const columnSizes = [];\n const tableHeaders = uiRoot.querySelectorAll('th');\n tableHeaders.forEach(header => {\n // Only get the width set via style attribute (set by move action).\n const width = parseInt(header.style.width);\n if (!width || isNaN(width)) {\n return;\n }\n columnSizes.push({\n column: header.dataset.columnid,\n width: width\n });\n });\n return JSON.stringify(columnSizes);\n};\n\n/**\n * Render resize handles in each container.\n *\n * This takes a list of the resize actions rendered in each column header, and creates a corresponding drag handle for each.\n * It also initialises the event handlers for the drag handles and resize modal.\n *\n * @param {Element} uiRoot Question bank UI root element.\n */\nconst setUpResizeHandles = (uiRoot) => {\n const resizeActions = uiRoot.querySelectorAll(SELECTORS.resizeAction);\n resizeActions.forEach(resizeAction => {\n const headerContainer = resizeAction.closest(SELECTORS.headerContainer);\n const handleContainer = headerContainer.querySelector(SELECTORS.handleContainer);\n const context = {\n action: \"resize\",\n target: '',\n title: resizeAction.title,\n pixicon: 'i/twoway',\n pixcomponent: 'core',\n popup: true\n };\n return Templates.renderForPromise('qbank_columnsortorder/action_handle', context)\n .then(({html, js}) => {\n Templates.appendNodeContents(handleContainer, html, js);\n return handleContainer;\n }).catch(Notification.exception);\n });\n\n let moveTracker = false;\n let currentResizeHandle = null;\n // Start mouse event on headers.\n uiRoot.addEventListener('mousedown', e => {\n currentResizeHandle = e.target.closest(SELECTORS.resizeHandle);\n // Return if it is not ' resize' button.\n if (!currentResizeHandle) {\n return;\n }\n // Save current position.\n currentX = e.pageX;\n // Find the header.\n currentHeader = e.target.closest(actions.SELECTORS.sortableColumn);\n moveTracker = false;\n suspendShowHideHandles = true;\n });\n\n // Resize column as the mouse move.\n document.addEventListener('mousemove', e => {\n if (!currentHeader || !currentResizeHandle || currentX === 0) {\n return;\n }\n\n // Prevent text selection as the handle is dragged.\n document.getSelection().removeAllRanges();\n\n // Adjust the column width according the amount the handle was dragged.\n const offset = e.pageX - currentX;\n currentX = e.pageX;\n const newWidth = currentHeader.offsetWidth + offset;\n currentHeader.style.width = newWidth + 'px';\n moveTracker = true;\n });\n\n // Set new size when mouse is up.\n document.addEventListener('mouseup', () => {\n if (!currentHeader || !currentResizeHandle || currentX === 0) {\n return;\n }\n if (moveTracker) {\n // If the mouse moved, we are changing the size by drag, so save the change.\n repository.setColumnSize(serialiseColumnSizes(uiRoot)).catch(Notification.exception);\n } else {\n // If the mouse didn't move, display a modal to change the size using a form.\n showResizeModal(currentHeader, uiRoot);\n }\n currentHeader = null;\n currentResizeHandle = null;\n currentX = 0;\n moveTracker = false;\n suspendShowHideHandles = false;\n });\n};\n\n/**\n * Event handler for resize actions in each column header.\n *\n * This will listen for a click on any resize action, and activate the corresponding resize modal.\n *\n * @param {Element} uiRoot Question bank UI root element.\n */\nconst setUpResizeActions = uiRoot => {\n uiRoot.addEventListener('click', (e) => {\n const resizeAction = e.target.closest(SELECTORS.resizeAction);\n if (resizeAction) {\n e.preventDefault();\n const currentHeader = resizeAction.closest('th');\n showResizeModal(currentHeader, uiRoot);\n }\n });\n};\n\n/**\n * Show a modal containing a number input for changing a column width without click-and-drag.\n *\n * @param {Element} currentHeader The header element that is being resized.\n * @param {Element} uiRoot The question bank UI root element.\n * @returns {Promise}\n */\nconst showResizeModal = async(currentHeader, uiRoot) => {\n const initialWidth = currentHeader.offsetWidth;\n\n const modal = await ModalSaveCancel.create({\n title: getString('resizecolumn', 'qbank_columnsortorder', currentHeader.textContent),\n body: Templates.render('qbank_columnsortorder/resize_modal', {}),\n show: true,\n });\n const root = modal.getRoot();\n root.on(ModalEvents.cancel, () => {\n currentHeader.style.width = `${initialWidth}px`;\n });\n root.on(ModalEvents.save, () => {\n repository.setColumnSize(serialiseColumnSizes(uiRoot)).catch(Notification.exception);\n });\n\n const body = await modal.bodyPromise;\n const input = body.get(0).querySelector('input');\n input.value = initialWidth;\n\n input.addEventListener('change', e => {\n const newWidth = e.target.value;\n currentHeader.style.width = `${newWidth}px`;\n });\n};\n\n/**\n * Event handler for move actions in each column header.\n *\n * This will listen for a click on any move action, pass the click to the corresponding move handle, causing its modal to be shown.\n *\n * @param {Element} uiRoot Question bank UI root element.\n */\nconst setUpMoveActions = uiRoot => {\n uiRoot.addEventListener('click', e => {\n const moveAction = e.target.closest(SELECTORS.moveAction);\n if (moveAction) {\n e.preventDefault();\n const sortableColumn = moveAction.closest(actions.SELECTORS.sortableColumn);\n const moveHandle = sortableColumn.querySelector(actions.SELECTORS.moveHandler);\n moveHandle.click();\n }\n });\n};\n\n/**\n * Event handler for showing and hiding handles when the mouse is over a column header.\n *\n * Implementing this behaviour using the :hover CSS pseudoclass is not sufficient, as the mouse may move over the neighbouring\n * header while dragging, leading to some odd behaviour. This allows us to suspend the show/hide behaviour while a handle is being\n * dragged, and so keep the active handle visible until the drag is finished.\n *\n * @param {Element} uiRoot Question bank UI root element.\n */\nconst setupShowHideHandles = uiRoot => {\n let shownHeader = null;\n let tableHead = uiRoot.querySelector('thead');\n uiRoot.addEventListener('mouseover', e => {\n if (suspendShowHideHandles) {\n return;\n }\n const header = e.target.closest(actions.SELECTORS.sortableColumn);\n if (!header && !shownHeader) {\n return;\n }\n if (!header || header !== shownHeader) {\n tableHead.querySelector('.show-handles')?.classList.remove('show-handles');\n shownHeader = header;\n if (header) {\n header.classList.add('show-handles');\n }\n }\n });\n};\n\n/**\n * Event handler for sortable list DROP event.\n *\n * Find all table cells corresponding to the column of the dropped header, and move them to the new position.\n *\n * @param {Event} event\n */\nconst reorderColumns = event => {\n // Current header.\n const header = event.target;\n // Find the previous sibling of the header, which will be used when moving columns.\n const insertAfter = header.previousElementSibling;\n // Move columns.\n const uiRoot = document.querySelector(SELECTORS.uiRoot);\n const columns = uiRoot.querySelectorAll(SELECTORS.tableColumn(header.dataset.columnid));\n columns.forEach(column => {\n const row = column.parentElement;\n if (insertAfter) {\n // Find the column to insert after.\n const insertAfterColumn = row.querySelector(SELECTORS.tableColumn(insertAfter.dataset.columnid));\n // Insert the column.\n insertAfterColumn.after(column);\n } else {\n // Insert as the first child (first column in the table).\n row.insertBefore(column, row.firstChild);\n }\n });\n};\n\n/**\n * Initialize module\n *\n * Add containers for the drag handles to each column header, then render handles, enable show/hide behaviour, set up drag/drop\n * column sorting, then enable the move and resize modals to be triggered from menu actions.\n */\nexport const init = async() => {\n const uiRoot = document.getElementById('questionscontainer');\n await addHandleContainers(uiRoot);\n setUpMoveHandles(uiRoot.querySelectorAll(SELECTORS.moveAction));\n setUpResizeHandles(uiRoot);\n setupShowHideHandles(uiRoot);\n const sortableColumns = actions.setupSortableLists(uiRoot.querySelector(actions.SELECTORS.columnList));\n sortableColumns.on(SortableList.EVENTS.DROP, reorderColumns);\n sortableColumns.on(SortableList.EVENTS.DRAGSTART, () => {\n suspendShowHideHandles = true;\n });\n sortableColumns.on(SortableList.EVENTS.DRAGEND, () => {\n suspendShowHideHandles = false;\n });\n setUpMoveActions(uiRoot);\n setUpResizeActions(uiRoot);\n actions.setupActionButtons(uiRoot);\n};\n"],"names":["SELECTORS","identifier","replace","currentHeader","currentX","suspendShowHideHandles","serialiseColumnSizes","uiRoot","columnSizes","querySelectorAll","forEach","header","width","parseInt","style","isNaN","push","column","dataset","columnid","JSON","stringify","showResizeModal","async","initialWidth","offsetWidth","modal","ModalSaveCancel","create","title","textContent","body","Templates","render","show","root","getRoot","on","ModalEvents","cancel","save","repository","setColumnSize","catch","Notification","exception","input","bodyPromise","get","querySelector","value","addEventListener","e","newWidth","target","reorderColumns","event","insertAfter","previousElementSibling","document","row","parentElement","after","insertBefore","firstChild","getElementById","Promise","resolve","headerContainers","renderForPromise","then","_ref","html","js","container","prependNodeContents","addHandleContainers","moveAction","closest","classList","add","handleContainer","context","action","dragtype","pixicon","pixcomponent","popup","_ref2","resizeAction","_ref3","appendNodeContents","moveTracker","currentResizeHandle","pageX","actions","sortableColumn","getSelection","removeAllRanges","offset","setUpResizeHandles","shownHeader","tableHead","remove","setupShowHideHandles","sortableColumns","setupSortableLists","columnList","SortableList","EVENTS","DROP","DRAGSTART","DRAGEND","preventDefault","moveHandler","click","setUpMoveActions","setUpResizeActions","setupActionButtons"],"mappings":";;;;;;;;0bAkCMA,iBACM,sBADNA,qBAEU,iCAFVA,uBAGY,mCAHZA,uBAIY,8CAJZA,0BAKe,oBALfA,0BAMe,oBANfA,sBAOWC,wCAAmCA,WAAWC,QAAQ,SAAU,kBAI7EC,cAGAC,SAOAC,wBAAyB,QA2DvBC,qBAAwBC,eACpBC,YAAc,UACCD,OAAOE,iBAAiB,MAChCC,SAAQC,eAEXC,MAAQC,SAASF,OAAOG,MAAMF,OAC/BA,QAASG,MAAMH,QAGpBJ,YAAYQ,KAAK,CACbC,OAAQN,OAAOO,QAAQC,SACvBP,MAAOA,WAGRQ,KAAKC,UAAUb,cA8GpBc,gBAAkBC,MAAMpB,cAAeI,gBACnCiB,aAAerB,cAAcsB,YAE7BC,YAAcC,2BAAgBC,OAAO,CACvCC,OAAO,mBAAU,eAAgB,wBAAyB1B,cAAc2B,aACxEC,KAAMC,mBAAUC,OAAO,qCAAsC,IAC7DC,MAAM,IAEJC,KAAOT,MAAMU,UACnBD,KAAKE,GAAGC,sBAAYC,QAAQ,KACxBpC,cAAcW,MAAMF,gBAAWY,sBAEnCW,KAAKE,GAAGC,sBAAYE,MAAM,KACtBC,WAAWC,cAAcpC,qBAAqBC,SAASoC,MAAMC,sBAAaC,oBAIxEC,aADapB,MAAMqB,aACNC,IAAI,GAAGC,cAAc,SACxCH,MAAMI,MAAQ1B,aAEdsB,MAAMK,iBAAiB,UAAUC,UACvBC,SAAWD,EAAEE,OAAOJ,MAC1B/C,cAAcW,MAAMF,gBAAWyC,mBA4DjCE,eAAiBC,cAEb7C,OAAS6C,MAAMF,OAEfG,YAAc9C,OAAO+C,uBAEZC,SAASV,cAAcjD,kBACfS,iBAAiBT,sBAAsBW,OAAOO,QAAQC,WACrET,SAAQO,eACN2C,IAAM3C,OAAO4C,iBACfJ,YAAa,CAEaG,IAAIX,cAAcjD,sBAAsByD,YAAYvC,QAAQC,WAEpE2C,MAAM7C,aAGxB2C,IAAIG,aAAa9C,OAAQ2C,IAAII,8BAWrBzC,gBACVhB,OAASoD,SAASM,eAAe,2BA9Rf1D,CAAAA,QACjB,IAAI2D,SAASC,gBACVC,iBAAmB7D,OAAOE,iBAAiBT,8CACvCqE,iBAAiB,yCAA0C,IAChEC,MAAKC,WAACC,KAACA,KAADC,GAAOA,gBACVL,iBAAiB1D,SAAQgE,+BACXC,oBAAoBD,UAAWF,KAAMC,OAEnDN,UACOC,oBACRzB,MAAMC,sBAAaC,cAqRxB+B,CAAoBrE,QACTA,OAAOE,iBAAiBT,sBA1Q7BU,SAAQmE,mBACVlE,OAASkE,WAAWC,QAAQ,MAClCnE,OAAOoE,UAAUC,IAAI,+BACfC,gBAAkBtE,OAAOsC,cAAcjD,2BACvCkF,QAAU,CACZC,OAAQ,OACRC,SAAU,OACV9B,OAAQ,GACRzB,MAAOgD,WAAWhD,MAClBwD,QAAS,aACTC,aAAc,OACdC,OAAO,UAEJvD,mBAAUqC,iBAAiB,sCAAuCa,SACpEZ,MAAKkB,YAAChB,KAACA,KAADC,GAAOA,oCACAE,oBAAoBM,gBAAiBT,KAAMC,IAC9CQ,mBACRtC,MAAMC,sBAAaC,cAqCNtC,CAAAA,SACFA,OAAOE,iBAAiBT,wBAChCU,SAAQ+E,qBAEZR,gBADkBQ,aAAaX,QAAQ9E,2BACLiD,cAAcjD,2BAChDkF,QAAU,CACZC,OAAQ,SACR7B,OAAQ,GACRzB,MAAO4D,aAAa5D,MACpBwD,QAAS,WACTC,aAAc,OACdC,OAAO,UAEJvD,mBAAUqC,iBAAiB,sCAAuCa,SACpEZ,MAAKoB,YAAClB,KAACA,KAADC,GAAOA,oCACAkB,mBAAmBV,gBAAiBT,KAAMC,IAC7CQ,mBACRtC,MAAMC,sBAAaC,kBAG1B+C,aAAc,EACdC,oBAAsB,KAE1BtF,OAAO4C,iBAAiB,aAAaC,IACjCyC,oBAAsBzC,EAAEE,OAAOwB,QAAQ9E,wBAElC6F,sBAILzF,SAAWgD,EAAE0C,MAEb3F,cAAgBiD,EAAEE,OAAOwB,QAAQiB,QAAQ/F,UAAUgG,gBACnDJ,aAAc,EACdvF,wBAAyB,MAI7BsD,SAASR,iBAAiB,aAAaC,QAC9BjD,gBAAkB0F,qBAAoC,IAAbzF,gBAK9CuD,SAASsC,eAAeC,wBAGlBC,OAAS/C,EAAE0C,MAAQ1F,SACzBA,SAAWgD,EAAE0C,YACPzC,SAAWlD,cAAcsB,YAAc0E,OAC7ChG,cAAcW,MAAMF,MAAQyC,SAAW,KACvCuC,aAAc,KAIlBjC,SAASR,iBAAiB,WAAW,KAC5BhD,eAAkB0F,qBAAoC,IAAbzF,WAG1CwF,YAEAnD,WAAWC,cAAcpC,qBAAqBC,SAASoC,MAAMC,sBAAaC,WAG1EvB,gBAAgBnB,cAAeI,QAEnCJ,cAAgB,KAChB0F,oBAAsB,KACtBzF,SAAW,EACXwF,aAAc,EACdvF,wBAAyB,OA+I7B+F,CAAmB7F,QA5DMA,CAAAA,aACrB8F,YAAc,KACdC,UAAY/F,OAAO0C,cAAc,SACrC1C,OAAO4C,iBAAiB,aAAaC,OAC7B/C,oCAGEM,OAASyC,EAAEE,OAAOwB,QAAQiB,QAAQ/F,UAAUgG,2CAC7CrF,QAAW0F,eAGX1F,QAAUA,SAAW0F,4CACtBC,UAAUrD,cAAc,yEAAkB8B,UAAUwB,OAAO,gBAC3DF,YAAc1F,OACVA,QACAA,OAAOoE,UAAUC,IAAI,sBA8CjCwB,CAAqBjG,cACfkG,gBAAkBV,QAAQW,mBAAmBnG,OAAO0C,cAAc8C,QAAQ/F,UAAU2G,aAC1FF,gBAAgBpE,GAAGuE,uBAAaC,OAAOC,KAAMvD,gBAC7CkD,gBAAgBpE,GAAGuE,uBAAaC,OAAOE,WAAW,KAC9C1G,wBAAyB,KAE7BoG,gBAAgBpE,GAAGuE,uBAAaC,OAAOG,SAAS,KAC5C3G,wBAAyB,KAzFRE,CAAAA,SACrBA,OAAO4C,iBAAiB,SAASC,UACvByB,WAAazB,EAAEE,OAAOwB,QAAQ9E,sBAChC6E,aACAzB,EAAE6D,iBACqBpC,WAAWC,QAAQiB,QAAQ/F,UAAUgG,gBAC1B/C,cAAc8C,QAAQ/F,UAAUkH,aACvDC,aAoFnBC,CAAiB7G,QA9IMA,CAAAA,SACvBA,OAAO4C,iBAAiB,SAAUC,UACxBqC,aAAerC,EAAEE,OAAOwB,QAAQ9E,2BAClCyF,aAAc,CACdrC,EAAE6D,uBACI9G,cAAgBsF,aAAaX,QAAQ,MAC3CxD,gBAAgBnB,cAAeI,aAyIvC8G,CAAmB9G,QACnBwF,QAAQuB,mBAAmB/G"} \ No newline at end of file diff --git a/question/bank/columnsortorder/amd/src/actions.js b/question/bank/columnsortorder/amd/src/actions.js new file mode 100644 index 00000000000..cc41521d546 --- /dev/null +++ b/question/bank/columnsortorder/amd/src/actions.js @@ -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 . + +/** + * 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 + * @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('&', '&')); + 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); +}; diff --git a/question/bank/columnsortorder/amd/src/admin_actions.js b/question/bank/columnsortorder/amd/src/admin_actions.js new file mode 100644 index 00000000000..e1c9215483f --- /dev/null +++ b/question/bank/columnsortorder/amd/src/admin_actions.js @@ -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 . + +/** + * Javascript for handling actions on the admin page + * + * @module qbank_columnsortorder/admin_actions + * @copyright 2023 onwards Catalyst IT Europe Ltd + * @author Mark Johnson + * @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); +}; diff --git a/question/bank/columnsortorder/amd/src/repository.js b/question/bank/columnsortorder/amd/src/repository.js new file mode 100644 index 00000000000..8c23c671d13 --- /dev/null +++ b/question/bank/columnsortorder/amd/src/repository.js @@ -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 . + +/** + * External function calls for qbank_columnsortorder + * + * @module qbank_columnsortorder/repository + * @copyright 2023 Catalyst IT Europe Ltd. + * @author Mark Johnson + * @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, + }, + }, + ]) +); diff --git a/question/bank/columnsortorder/amd/src/sort_columns.js b/question/bank/columnsortorder/amd/src/sort_columns.js deleted file mode 100644 index 3e2859bef6f..00000000000 --- a/question/bank/columnsortorder/amd/src/sort_columns.js +++ /dev/null @@ -1,79 +0,0 @@ -// 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 . - -/** - * Javascript for sorting columns in question bank view. - * - * @copyright 2021 Catalyst IT Australia Pty Ltd - * @author Ghaly Marc-Alexandre - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -import {call as fetchMany} from 'core/ajax'; -import {exception as displayException} from 'core/notification'; -import SortableList from 'core/sortable_list'; -import jQuery from 'jquery'; - -/** - * Sets up sortable list in the column sort order page. - * @param {Element} listRoot - */ -const setupSortableLists = (listRoot) => { - new SortableList('.list', { - moveHandlerSelector: '.item', - }); - - jQuery('.item').on(SortableList.EVENTS.DROP, () => { - const columns = getColumnOrder(listRoot); - setOrder(columns).catch(displayException); - listRoot.querySelectorAll('.item').forEach(item => item.classList.remove('active')); - }); - - jQuery('.item').on(SortableList.EVENTS.DRAGSTART, (event) => { - event.currentTarget.classList.add('active'); - }); -}; - -/** - * Call external function set_order - inserts the updated column in the config_plugins table. - * - * @param {String} columns String that contains column order. - * @returns {Promise} - */ -const setOrder = columns => fetchMany([{ - methodname: 'qbank_columnsortorder_set_columnbank_order', - args: {columns}, -}])[0]; - -/** - * Gets the newly reordered columns to display in the question bank view. - * @param {Element} listRoot - * @returns {Array} - */ -const getColumnOrder = listRoot => { - const columns = Array.from(listRoot.querySelectorAll('[data-pluginname]')) - .map(column => column.dataset.pluginname); - - return columns.filter((value, index) => columns.indexOf(value) === index); -}; - -/** - * Initialize module - * @param {String} id unique id for columns. - */ -export const init = id => { - const listRoot = document.querySelector(`#${id}`); - setupSortableLists(listRoot); -}; diff --git a/question/bank/columnsortorder/amd/src/user_actions.js b/question/bank/columnsortorder/amd/src/user_actions.js new file mode 100644 index 00000000000..0dbefc6b252 --- /dev/null +++ b/question/bank/columnsortorder/amd/src/user_actions.js @@ -0,0 +1,366 @@ +// 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 . + +/** + * 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 + * @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; + +/** + * 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); +}; + +/** + * 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 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); + 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; + 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); + } + 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} + */ +const showResizeModal = async(currentHeader, uiRoot) => { + const initialWidth = currentHeader.offsetWidth; + + const modal = await ModalSaveCancel.create({ + title: getString('resizecolumn', 'qbank_columnsortorder', currentHeader.textContent), + body: Templates.render('qbank_columnsortorder/resize_modal', {}), + 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.value = initialWidth; + + input.addEventListener('change', e => { + 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); +}; diff --git a/question/bank/columnsortorder/classes/column_manager.php b/question/bank/columnsortorder/classes/column_manager.php index 1dde60d9c51..c731f9820fa 100644 --- a/question/bank/columnsortorder/classes/column_manager.php +++ b/question/bank/columnsortorder/classes/column_manager.php @@ -21,8 +21,14 @@ 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; /** @@ -33,40 +39,130 @@ use moodle_url; * @author Ghaly Marc-Alexandre * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class column_manager { +class column_manager extends column_manager_base { /** - * @var array|bool Column order as set in config_plugins 'class' => 'position', ie: question_type_column => 3. + * @var array Column order as set in config_plugins 'class' => 'position', ie: question_type_column => 3. */ public $columnorder; /** - * @var array|bool Disabled columns in config_plugins table. + * @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() { - $this->columnorder = get_config('qbank_columnsortorder', 'enabledcol'); - $this->disabledcolumns = get_config('qbank_columnsortorder', 'disabledcol'); + 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(explode(',', $this->columnorder)); + $this->columnorder = array_flip($this->columnorder); } if ($this->disabledcolumns) { - $this->disabledcolumns = array_flip(explode(',', $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. + * @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) : void { - $columns = implode(',', $columns); - set_config('enabledcol', $columns, 'qbank_columnsortorder'); + 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); + } } /** @@ -74,12 +170,20 @@ class column_manager { * * @return view */ - protected function get_questionbank(): 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 view($contexts, new moodle_url('/question/dummyurl.php'), $course, null); + $questionbank = new preview_view( + $contexts, + new moodle_url('/question/bank/columnsortorder/sortcolumns.php'), + $course, + null, + $params + ); return $questionbank; } @@ -94,11 +198,11 @@ class column_manager { if ($column->get_name() === 'checkbox') { continue; } - $classelements = explode('\\', $key); $columns[] = (object) [ 'class' => get_class($column), 'name' => $column->get_title(), - 'colname' => end($classelements), + 'colname' => $column->get_column_name(), + 'id' => $column->get_column_id(), ]; } return $columns; @@ -112,20 +216,12 @@ class column_manager { public function get_disabled_columns(): array { $disabled = []; if ($this->disabledcolumns) { - foreach ($this->disabledcolumns as $class => $value) { - if (strpos($class, 'qbank_customfields\custom_field_column') !== false) { - $class = explode('\\', $class); - $disabledname = array_pop($class); - $class = implode('\\', $class); - $disabled[] = (object) [ - 'disabledname' => $disabledname, - ]; - } else { - $columnobject = new $class($this->get_questionbank()); - $disabled[] = (object) [ - 'disabledname' => $columnobject->get_title(), - ]; - } + foreach (array_keys($this->disabledcolumns) as $column) { + [$classname, $columnname] = explode(column_base::ID_SEPARATOR, $column, 2); + $columnobject = $classname::from_column_name($this->get_questionbank(), $columnname); + $disabled[$column] = (object) [ + 'disabledname' => $columnobject->get_title(), + ]; } } return $disabled; @@ -192,17 +288,10 @@ class column_manager { } foreach ($allcolumns as $column) { - if (strpos($column->class, $plugin) !== false) { - if ($column->class === 'qbank_customfields\custom_field_column') { - $disabledcolumns[$column->class . '\\' . $column->colname] = $column->class . '\\' . $column->colname; - if (isset($enabledcolumns[$column->class . '\\' . $column->colname])) { - unset($enabledcolumns[$column->class. '\\' . $column->colname]); - } - } else { - $disabledcolumns[$column->class] = $column->class; - if (isset($enabledcolumns[$column->class])) { - unset($enabledcolumns[$column->class]); - } + if (str_contains($column->class, $plugin)) { + $disabledcolumns[$column->id] = $column->id; + if (isset($enabledcolumns[$column->id])) { + unset($enabledcolumns[$column->id]); } } } @@ -212,7 +301,7 @@ class column_manager { /** * Orders columns in the question bank view according to config_plugins table 'qbank_columnsortorder' config. * - * @param array $ordertosort Unordered array of columns + * @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 { @@ -222,32 +311,84 @@ class column_manager { $columnsortorder = $this->columnorder; asort($columnsortorder); $columnorder = []; - foreach ($columnsortorder as $classname => $colposition) { - $colname = explode('\\', $classname); - if (strpos($classname, 'qbank_customfields\custom_field_column') !== false) { - unset($colname[0]); - $classname = implode('\\', $colname); - // Checks if custom column still exists. - if (array_key_exists($classname, $ordertosort)) { - $columnorder[$classname] = $colposition; - } else { - $configtounset = str_replace('\\', '\\\\', $classname); - // Cleans config db. - unset_config($configtounset, 'column_sortorder'); - } - } else { - $columnorder[end($colname)] = $colposition; + 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. - if (isset($properorder['checkbox_column'])) { - $checkboxfirstelement = $properorder['checkbox_column']; - unset($properorder['checkbox_column']); - $properorder = array_merge(['checkbox_column' => $checkboxfirstelement], $properorder); + $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 { + return array_reduce($this->hiddencolumns, function($result, $hiddencolumn) { + [$columnclass, $columnname] = explode(column_base::ID_SEPARATOR, $hiddencolumn, 2); + $result[$hiddencolumn] = $columnclass::from_column_name($this->get_questionbank(), $columnname)->get_title(); + 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), + ]; + } } diff --git a/question/bank/columnsortorder/classes/external/set_column_size.php b/question/bank/columnsortorder/classes/external/set_column_size.php new file mode 100644 index 00000000000..9ea26f82bc6 --- /dev/null +++ b/question/bank/columnsortorder/classes/external/set_column_size.php @@ -0,0 +1,89 @@ +. + +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 + * @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); + } +} diff --git a/question/bank/columnsortorder/classes/external/set_columnbank_order.php b/question/bank/columnsortorder/classes/external/set_columnbank_order.php index a0f9613e2ff..3e06fd737d7 100644 --- a/question/bank/columnsortorder/classes/external/set_columnbank_order.php +++ b/question/bank/columnsortorder/classes/external/set_columnbank_order.php @@ -41,8 +41,14 @@ class set_columnbank_order extends external_api { 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) - ) + 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), ]); } @@ -54,16 +60,26 @@ class set_columnbank_order extends external_api { } /** - * Returns the columns plugin order. + * Set columns order. * - * @param array $columns json string representing new column 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): void { - ['columns' => $columns] = self::validate_parameters(self::execute_parameters(), ['columns' => $columns]); + 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); - require_capability('moodle/category:manage', $context); + if ($global) { + require_capability('moodle/site:config', $context); + } - column_manager::set_column_order($columns); + column_manager::set_column_order($columns, $global); } } diff --git a/question/bank/columnsortorder/classes/external/set_hidden_columns.php b/question/bank/columnsortorder/classes/external/set_hidden_columns.php new file mode 100644 index 00000000000..df729aa010e --- /dev/null +++ b/question/bank/columnsortorder/classes/external/set_hidden_columns.php @@ -0,0 +1,92 @@ +. + +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 + * @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); + } +} diff --git a/question/bank/columnsortorder/classes/local/bank/column_action_move.php b/question/bank/columnsortorder/classes/local/bank/column_action_move.php new file mode 100644 index 00000000000..f086f41d660 --- /dev/null +++ b/question/bank/columnsortorder/classes/local/bank/column_action_move.php @@ -0,0 +1,53 @@ +. + +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 + * @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'), + null, + $this->move, + [ + 'title' => get_string('movecolumn', 'qbank_columnsortorder', $column->get_title()), + 'data-action' => 'move', + 'data-column' => get_class($column), + ] + ); + } +} diff --git a/question/bank/columnsortorder/classes/local/bank/column_action_remove.php b/question/bank/columnsortorder/classes/local/bank/column_action_remove.php new file mode 100644 index 00000000000..ae8a28c7677 --- /dev/null +++ b/question/bank/columnsortorder/classes/local/bank/column_action_remove.php @@ -0,0 +1,78 @@ +. + +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 + * @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, + null, + $this->remove, + [ + 'class' => 'action-link', + 'title' => get_string('removecolumn', 'qbank_columnsortorder', $column->get_title()), + 'data-action' => 'remove', + 'data-column' => $column->get_column_id(), + ] + ); + } +} diff --git a/question/bank/columnsortorder/classes/local/bank/column_action_resize.php b/question/bank/columnsortorder/classes/local/bank/column_action_resize.php new file mode 100644 index 00000000000..4ddcc4124ee --- /dev/null +++ b/question/bank/columnsortorder/classes/local/bank/column_action_resize.php @@ -0,0 +1,53 @@ +. + +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 + * @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'), + null, + $this->resize, + [ + 'title' => get_string('resizecolumn', 'qbank_columnsortorder', $column->get_title()), + 'data-action' => 'resize', + 'data-column' => get_class($column), + ] + ); + } +} diff --git a/question/bank/columnsortorder/classes/local/bank/preview_view.php b/question/bank/columnsortorder/classes/local/bank/preview_view.php new file mode 100644 index 00000000000..d6a4e28a911 --- /dev/null +++ b/question/bank/columnsortorder/classes/local/bank/preview_view.php @@ -0,0 +1,99 @@ +. + +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 + * @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(); + } +} diff --git a/question/bank/columnsortorder/classes/output/add_column.php b/question/bank/columnsortorder/classes/output/add_column.php new file mode 100644 index 00000000000..2bda9ddbcc9 --- /dev/null +++ b/question/bank/columnsortorder/classes/output/add_column.php @@ -0,0 +1,77 @@ +. + +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 + * @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, + ]; + } +} diff --git a/question/bank/columnsortorder/classes/output/column_sort_preview.php b/question/bank/columnsortorder/classes/output/column_sort_preview.php new file mode 100644 index 00000000000..6e29aabc506 --- /dev/null +++ b/question/bank/columnsortorder/classes/output/column_sort_preview.php @@ -0,0 +1,54 @@ +. + +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 + * @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; + } +} diff --git a/question/bank/columnsortorder/classes/output/column_sort_ui.php b/question/bank/columnsortorder/classes/output/column_sort_ui.php new file mode 100644 index 00000000000..372fcdb3e1d --- /dev/null +++ b/question/bank/columnsortorder/classes/output/column_sort_ui.php @@ -0,0 +1,94 @@ +. + +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 + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class column_sort_ui implements renderable, templatable { + /** + * @var int The minimum custom width for a column. + */ + const MIN_COLUMN_WIDTH = 10; + + 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; + } +} diff --git a/question/bank/columnsortorder/classes/output/fragment.php b/question/bank/columnsortorder/classes/output/fragment.php new file mode 100644 index 00000000000..33fc3653063 --- /dev/null +++ b/question/bank/columnsortorder/classes/output/fragment.php @@ -0,0 +1,37 @@ +. + +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 + * @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()); + } +} diff --git a/question/bank/columnsortorder/classes/output/renderer.php b/question/bank/columnsortorder/classes/output/renderer.php deleted file mode 100644 index 76354228944..00000000000 --- a/question/bank/columnsortorder/classes/output/renderer.php +++ /dev/null @@ -1,57 +0,0 @@ -. - -namespace qbank_columnsortorder\output; - -use moodle_url; -use plugin_renderer_base; -use qbank_columnsortorder\column_manager; - -/** - * Class renderer. - * @package qbank_columnsortorder - * @copyright 2021 Catalyst IT Australia Pty Ltd - * @author Ghaly Marc-Alexandre - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class renderer extends plugin_renderer_base { - /** - * Render list of question bank plugin columns. - * - * @return string The rendered HTML. - */ - public function render_column_sort_ui() { - $columnsortorder = new column_manager(); - $enabledcolumns = $columnsortorder->get_columns(); - $disabledcolumns = $columnsortorder->get_disabled_columns(); - $params = []; - foreach ($enabledcolumns as $columnname) { - $name = $columnname->name; - $colname = get_string('qbankcolumnname', 'qbank_columnsortorder', $columnname->colname); - if ($columnname->class === 'qbank_customfields\custom_field_column') { - $columnname->class .= "\\$columnname->colname"; - } - $params['names'][] = ['name' => $name, 'colname' => $colname, 'class' => $columnname->class]; - } - $params['disabled'] = $disabledcolumns; - $params['columnsdisabled'] = (!empty($params['disabled'])) ? true : false; - $urltoredirect = new moodle_url('/admin/settings.php', ['section' => 'manageqbanks']); - - $params['urltomanageqbanks'] = get_string('qbankgotomanageqbanks', 'qbank_columnsortorder', $urltoredirect->out()); - - return $this->render_from_template('qbank_columnsortorder/columnsortorder', $params); - } -} diff --git a/question/bank/columnsortorder/classes/output/reset_columns.php b/question/bank/columnsortorder/classes/output/reset_columns.php new file mode 100644 index 00000000000..1cecdf5f8d0 --- /dev/null +++ b/question/bank/columnsortorder/classes/output/reset_columns.php @@ -0,0 +1,60 @@ +. + +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 + * @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), + ]; + } +} diff --git a/question/bank/columnsortorder/classes/plugin_feature.php b/question/bank/columnsortorder/classes/plugin_feature.php new file mode 100644 index 00000000000..11a23a442bb --- /dev/null +++ b/question/bank/columnsortorder/classes/plugin_feature.php @@ -0,0 +1,64 @@ +. + +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 + * @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), + ]; + } +} diff --git a/question/bank/columnsortorder/classes/privacy/provider.php b/question/bank/columnsortorder/classes/privacy/provider.php index 9a7e367de3a..29d4f3fd0d6 100644 --- a/question/bank/columnsortorder/classes/privacy/provider.php +++ b/question/bank/columnsortorder/classes/privacy/provider.php @@ -16,7 +16,8 @@ namespace qbank_columnsortorder\privacy; -use \core_privacy\local\metadata\null_provider; +use core_privacy\local\metadata\collection; +use core_privacy\local\request\writer; /** * Privacy provider for columnsortorder. @@ -26,14 +27,32 @@ use \core_privacy\local\metadata\null_provider; * @author Ghaly Marc-Alexandre * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class provider implements null_provider { - /** - * Get the language string identifier with the component's language - * file to explain why this plugin stores no data. - * - * @return string - */ - public static function get_reason() : string { - return 'privacy:metadata'; +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); + } + } + } } } diff --git a/question/bank/columnsortorder/classes/tests/external_function_testcase.php b/question/bank/columnsortorder/classes/tests/external_function_testcase.php new file mode 100644 index 00000000000..b84d5cb3db1 --- /dev/null +++ b/question/bank/columnsortorder/classes/tests/external_function_testcase.php @@ -0,0 +1,92 @@ +. + +/** + * Class defining resuable tests methods for external functions + * + * @package qbank_columnsortorder + * @copyright 2023 Catalyst IT Europe Ltd. + * @author Mark Johnson + * @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); + } +} diff --git a/question/bank/columnsortorder/db/services.php b/question/bank/columnsortorder/db/services.php index 11484e65c6b..f331fa4e987 100644 --- a/question/bank/columnsortorder/db/services.php +++ b/question/bank/columnsortorder/db/services.php @@ -14,9 +14,9 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . - /** * qbank_columnsortorder external functions and service definitions. + * * @package qbank_columnsortorder * @category webservice * @copyright 2021 Catalyst IT Australia Pty Ltd @@ -28,9 +28,21 @@ defined('MOODLE_INTERNAL') || die(); $functions = [ 'qbank_columnsortorder_set_columnbank_order' => [ - 'classname' => 'qbank_columnsortorder\external\set_columnbank_order', + 'classname' => 'qbank_columnsortorder\external\set_columnbank_order', 'description' => 'Sets question columns order in database', - 'type' => 'write', - 'ajax' => true, + '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, ], ]; diff --git a/question/bank/columnsortorder/lang/en/qbank_columnsortorder.php b/question/bank/columnsortorder/lang/en/qbank_columnsortorder.php index 818e942dffe..eb4a9cc09a9 100644 --- a/question/bank/columnsortorder/lang/en/qbank_columnsortorder.php +++ b/question/bank/columnsortorder/lang/en/qbank_columnsortorder.php @@ -23,11 +23,25 @@ * @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 10.'; +$string['movecolumn'] = 'Move column \'{$a}\''; $string['pluginname'] = 'Column sort order'; -$string['privacy:metadata'] = 'The Column sort order question bank plugin does not store any personal data.'; +$string['privacy:metadata:preference:enabledcol'] = 'The plugin saves user preference of column orders.'; +$string['privacy:metadata:preference:hiddencols'] = 'The plugin saves user preference of hidden columns.'; +$string['privacy:metadata:preference:colsize'] = 'The 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 Manage question bank plugins.'; -$string['qbankcolumnname'] = '({$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'; diff --git a/question/bank/columnsortorder/lib.php b/question/bank/columnsortorder/lib.php new file mode 100644 index 00000000000..172780aa771 --- /dev/null +++ b/question/bank/columnsortorder/lib.php @@ -0,0 +1,36 @@ +. + +/** + * 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 + * @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(); +} diff --git a/question/bank/columnsortorder/sortcolumns.php b/question/bank/columnsortorder/sortcolumns.php index f3afb8e8336..7fd69d8b587 100644 --- a/question/bank/columnsortorder/sortcolumns.php +++ b/question/bank/columnsortorder/sortcolumns.php @@ -29,9 +29,16 @@ require_once($CFG->libdir.'/adminlib.php'); admin_externalpage_setup('qbank_columnsortorder', '', ['section' => 'columnsortorder'], '/question/bank/columnsortorder/sortcolumns.php'); -$renderer = $PAGE->get_renderer('qbank_columnsortorder'); +$preview = optional_param('preview', false, PARAM_BOOL); echo $OUTPUT->header(); echo $OUTPUT->heading(new lang_string('qbankcolumnsortorder', 'qbank_columnsortorder')); -echo $renderer->render_column_sort_ui(); +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(); diff --git a/question/bank/columnsortorder/styles.css b/question/bank/columnsortorder/styles.css new file mode 100644 index 00000000000..667f4912d58 --- /dev/null +++ b/question/bank/columnsortorder/styles.css @@ -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; +} diff --git a/question/bank/columnsortorder/templates/action_handle.mustache b/question/bank/columnsortorder/templates/action_handle.mustache new file mode 100644 index 00000000000..1aabe979ea1 --- /dev/null +++ b/question/bank/columnsortorder/templates/action_handle.mustache @@ -0,0 +1,46 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template 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 + } +}} + + {{#pixicon}} + {{#pix}}{{pixicon}}, {{pixcomponent}}{{/pix}} + {{/pixicon}} + {{#icon}} + + {{/icon}} + {{#text}} + {{text}} + {{/text}} + diff --git a/question/bank/columnsortorder/templates/add_column.mustache b/question/bank/columnsortorder/templates/add_column.mustache new file mode 100644 index 00000000000..aa2104abb3a --- /dev/null +++ b/question/bank/columnsortorder/templates/add_column.mustache @@ -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 . +}} +{{! + @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}} + +{{/hashiddencolumns}} diff --git a/question/bank/columnsortorder/templates/column_sort_preview.mustache b/question/bank/columnsortorder/templates/column_sort_preview.mustache new file mode 100644 index 00000000000..f0d43999cc2 --- /dev/null +++ b/question/bank/columnsortorder/templates/column_sort_preview.mustache @@ -0,0 +1,37 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template qbank_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": "
" + } +}} +
+ + Back + + {{{preview}}} +
diff --git a/question/bank/columnsortorder/templates/column_sort_ui.mustache b/question/bank/columnsortorder/templates/column_sort_ui.mustache new file mode 100644 index 00000000000..62e6fa88d3d --- /dev/null +++ b/question/bank/columnsortorder/templates/column_sort_ui.mustache @@ -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 . +}} +{{! + @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": "Manage qbank plugins" + } +}} +
+

+ {{#str}}qbanksortdescription, qbank_columnsortorder{{/str}} +

+
+ {{! + }}
+
+ + {{#str}}preview{{/str}} + + {{#addcolumn}} + {{>qbank_columnsortorder/add_column}} + {{/addcolumn}} +
+ {{#resetcolumns}} + {{>qbank_columnsortorder/reset_columns}} + {{/resetcolumns}} +
+ + + + + + + + + + + + {{#names}} + + + + + + + + {{/names}} + +
{{#str}}name{{/str}}{{#str}}plugin{{/str}}{{#str}}columnwidth, qbank_columnsortorder{{/str}}{{#str}}action{{/str}}
{{>core/drag_handle}}{{name}}{{colname}} +
+ + + +
+ {{#str}}invalidwidth, qbank_columnsortorder{{/str}} +
+
+
{{#actionmenu}}{{>core/action_menu}}{{/actionmenu}}
+
+
+ {{#columnsdisabled}} + {{#str}}qbankcolumnsdisabled, qbank_columnsortorder{{/str}} +
+ {{/columnsdisabled}} + {{#disabled}} +
+ {{disabledname}} +
+ {{/disabled}} +
+
+ {{{urltomanageqbanks}}} +
+ {{#js}} + require(['qbank_columnsortorder/admin_actions'], function(AdminTable) { + AdminTable.init("qbank_columnsortorder-{{uniqid}}"); + }); + {{/js}} +
diff --git a/question/bank/columnsortorder/templates/columnsortorder.mustache b/question/bank/columnsortorder/templates/columnsortorder.mustache deleted file mode 100644 index ece72884014..00000000000 --- a/question/bank/columnsortorder/templates/columnsortorder.mustache +++ /dev/null @@ -1,65 +0,0 @@ -{{! - This file is part of Moodle - http://moodle.org/ - - Moodle is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Moodle is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Moodle. If not, see . -}} -{{! - @template qbank_columnsortorder/columnsortorder.mustache - - Admin question bank setting template. - - Context variables required for this template: - * name - plugin name - * hiddenname - class/columname - - Example context (json): - { - "name": "Creatorname (creator_name_column)", - "hiddenname": "creator_name_column" - } -}} - -
- {{#str}}qbanksortdescription, qbank_columnsortorder{{/str}} -
-
-
- {{#names}} -
- {{>core/drag_handle}} - {{name}} {{colname}} -
- {{/names}} -
-
-
- {{#columnsdisabled}} - {{#str}}qbankcolumnsdisabled, qbank_columnsortorder{{/str}} -
- {{/columnsdisabled}} - {{#disabled}} -
- {{disabledname}} -
- {{/disabled}} -
-{{#js}} - require(['qbank_columnsortorder/sort_columns'], function(SortColumns) { - SortColumns.init("qbank_columnsortorder-{{uniqid}}"); - }); -{{/js}} -
-
- {{{urltomanageqbanks}}} -
diff --git a/question/bank/columnsortorder/templates/handle_container.mustache b/question/bank/columnsortorder/templates/handle_container.mustache new file mode 100644 index 00000000000..6657328e58e --- /dev/null +++ b/question/bank/columnsortorder/templates/handle_container.mustache @@ -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 . +}} +{{! + @template qbank_columnsortorder/handle_container + + Container for move/resize handles. + + Example context (json): + { + } +}} +
+
diff --git a/question/bank/columnsortorder/templates/reset_columns.mustache b/question/bank/columnsortorder/templates/reset_columns.mustache new file mode 100644 index 00000000000..f6cc991829c --- /dev/null +++ b/question/bank/columnsortorder/templates/reset_columns.mustache @@ -0,0 +1,33 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template qbank_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" + } +}} + + + {{#str}}resetcolumns, qbank_columnsortorder{{/str}} + + diff --git a/question/bank/columnsortorder/templates/resize_modal.mustache b/question/bank/columnsortorder/templates/resize_modal.mustache new file mode 100644 index 00000000000..88cace87bf6 --- /dev/null +++ b/question/bank/columnsortorder/templates/resize_modal.mustache @@ -0,0 +1,29 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template qbank_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): + { + } +}} + diff --git a/question/bank/columnsortorder/tests/behat/admin_settings.feature b/question/bank/columnsortorder/tests/behat/admin_settings.feature new file mode 100644 index 00000000000..affcb047c32 --- /dev/null +++ b/question/bank/columnsortorder/tests/behat/admin_settings.feature @@ -0,0 +1,118 @@ +@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 "" diff --git a/question/bank/columnsortorder/tests/behat/behat_qbank_columnsortorder.php b/question/bank/columnsortorder/tests/behat/behat_qbank_columnsortorder.php new file mode 100644 index 00000000000..a78949ec96d --- /dev/null +++ b/question/bank/columnsortorder/tests/behat/behat_qbank_columnsortorder.php @@ -0,0 +1,43 @@ +. + +// 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 + * @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')]", + ]), + ]; + } +} diff --git a/question/bank/columnsortorder/tests/behat/columnsortorder.feature b/question/bank/columnsortorder/tests/behat/columnsortorder.feature deleted file mode 100644 index cba9965b00d..00000000000 --- a/question/bank/columnsortorder/tests/behat/columnsortorder.feature +++ /dev/null @@ -1,92 +0,0 @@ -@qbank @qbank_columnsortorder @javascript -Feature: An plugin column can be reordered and displayed in the question bank view. - In order to reorganise the question bank view columns - As an admin - I need to be able to reorder them - - Background: - Given the following "users" exist: - | username | firstname | lastname | email | - | teacher1 | Teacher | 1 | teacher1@example.com | - And the following "courses" exist: - | fullname | shortname | format | - | Course 1 | C1 | weeks | - And the following "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 | - - Scenario: Teacher can see proper view - 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" - And I should see "Test question to be seen" - Then I should see "Teacher 1" - - Scenario: Reordering question bank columns - Given I log in as "admin" - When I navigate to "Plugins > Question bank plugins > Column sort order" in site administration - And I drag "Created by (creator_name_column)" "text" and I drop it in "T (question_type_column)" "text" - And I am on the "Test quiz Q001" "mod_quiz > question bank" page logged in as "teacher1" - And I apply question bank filter "Category" with value "Question category 1" - Then ".creatorname" "css_element" should appear before ".qtype" "css_element" - - Scenario: Disabling and enabling column display is proper - 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 (creator_name_column)" - 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 should not see "Created by (creator_name_column)" - 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 (creator_name_column)" - - Scenario: Custom fields are reorderable - Given I log in as "admin" - When 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" - Then I should see "checkboxcustomcolumn" - And I navigate to "Plugins > Question bank plugins > Column sort order" in site administration - And I should see "checkboxcustomcolumn" - And I change the window size to "large" - And I drag "checkboxcustomcolumn" "text" and I drop it in "T (question_type_column)" "text" - Then "checkboxcustomcolumn" "text" should appear before "T (question_type_column)" "text" - And I click on "Manage question bank plugins" "link" - And I click on "Disable" "link" in the "Question custom fields" "table_row" - And I click on "Column sort order" "link" - Then "Currently disabled question bank plugins:" "text" should appear before "chckcust" "text" - And I click on "Manage question bank plugins" "link" - And I click on "Enable" "link" in the "Question custom fields" "table_row" - And I click on "Column sort order" "link" - Then I should not see "Currently disabled question bank plugins:" - And I should see "checkboxcustomcolumn" - - 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. diff --git a/question/bank/columnsortorder/tests/behat/question_bank.feature b/question/bank/columnsortorder/tests/behat/question_bank.feature new file mode 100644 index 00000000000..93269279b14 --- /dev/null +++ b/question/bank/columnsortorder/tests/behat/question_bank.feature @@ -0,0 +1,143 @@ +@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. diff --git a/question/bank/columnsortorder/tests/column_manager_test.php b/question/bank/columnsortorder/tests/column_manager_test.php index d9c0bd1a03a..78b99e1b284 100644 --- a/question/bank/columnsortorder/tests/column_manager_test.php +++ b/question/bank/columnsortorder/tests/column_manager_test.php @@ -20,10 +20,10 @@ 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; -use qbank_columnsortorder\external\set_columnbank_order; global $CFG; require_once($CFG->dirroot . '/question/tests/fixtures/testable_core_question_column.php'); @@ -36,43 +36,147 @@ require_once($CFG->dirroot . '/question/classes/external.php'); * @copyright 2021 Catalyst IT Australia Pty Ltd * @author Ghaly Marc-Alexandre * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @coversDefaultClass \qbank_columnsortorder\column_manager + * @covers \qbank_columnsortorder\column_manager */ class column_manager_test extends advanced_testcase { - - /** @var \stdClass course record. */ - protected $course; - - /** @var \core_question\local\bank\view */ - protected $questionbank; - - /** @var array */ - protected $columns; - - /** @var \qbank_columnsortorder\column_manager */ - protected $columnmanager; + /** + * 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; + } /** - * Setup testcase. + * Return an array of visible columns for the question bank. + * + * @param view $questionbank + * @return array */ - public function setUp(): void { - $this->resetAfterTest(true); - $this->setAdminUser(); - $this->course = $this->getDataGenerator()->create_course(); - // Creates question bank view. - $this->questionbank = new view( - new question_edit_contexts(context_course::instance($this->course->id)), - new moodle_url('/'), - $this->course - ); - - // Get current view columns. - $this->columns = []; - foreach ($this->questionbank->get_visiblecolumns() as $columnn) { - $this->columns[] = get_class($columnn); + protected static function get_columns(view $questionbank): array { + $columns = []; + foreach ($questionbank->get_visiblecolumns() as $column) { + $columns[] = $column->get_column_id(); } - $this->columnmanager = new column_manager(); + return $columns; + } + + /** + * Provide examples for testing each column setting function, with test data and data format. + * + * @return array[] + */ + public static function settings_provider(): array { + $questionbank = self::get_question_bank(); + return [ + 'Test set_column_order' => [ + 'setting' => 'enabledcol', + 'function' => 'set_column_order', + 'data' => self::get_columns($questionbank), + 'csv' => true, + ], + 'Test set_hidden_columns' => [ + 'setting' => 'hiddencols', + 'function' => 'set_hidden_columns', + 'data' => self::get_columns($questionbank), + 'csv' => true, + ], + 'Test set_column_size' => [ + 'setting' => 'colsize', + 'function' => 'set_column_size', + 'data' => random_string(), + 'csv' => false, + ], + ]; + } + + /** + * 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 mixed $data 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, mixed $data, bool $csv): void { + $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 mixed $data 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, mixed $data, bool $csv): void { + $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 mixed $data The data 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, mixed $data, bool $csv): void { + $this->resetAfterTest(true); + $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 mixed $data 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, mixed $data, bool $csv): void { + $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)); } /** @@ -81,7 +185,10 @@ class column_manager_test extends advanced_testcase { * @covers ::get_columns */ public function test_getcolumns_function(): void { - $questionlistcolumns = $this->columnmanager->get_columns(); + $this->resetAfterTest(true); + $this->setAdminUser(); + $columnmanager = new column_manager(true); + $questionlistcolumns = $columnmanager->get_columns(); $this->assertIsArray($questionlistcolumns); foreach ($questionlistcolumns as $columnnobject) { $this->assertObjectHasAttribute('class', $columnnobject); @@ -91,45 +198,67 @@ class column_manager_test extends advanced_testcase { } /** - * Test function sort columns method. + * The get_sorted_columns method should return the provided columns sorted according to enabledcol setting. * - * @covers ::get_sorted_columns + * @return void */ public function test_get_sorted_columns(): void { - $neworder = $this->columnmanager->get_sorted_columns($this->columns); + $this->resetAfterTest(true); + $this->setAdminUser(); + $questionbank = $this->get_question_bank(); + $columns = $this->get_columns($questionbank); + $neworder = $columns; shuffle($neworder); - set_columnbank_order::execute($neworder); - $currentconfig = get_config('qbank_columnsortorder', 'enabledcol'); - $currentconfig = explode(',', $currentconfig); - ksort($currentconfig); - $this->assertSame($neworder, $currentconfig); + 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 function enabing and disablingcolumns. + * Test disabled columns are removed from enabledcol setting and added to disabledcol setting. * - * @covers ::enable_columns - * @covers ::disable_columns + * @return void */ - public function test_enable_disable_columns(): void { - $neworder = $this->columnmanager->get_sorted_columns($this->columns); - shuffle($neworder); - set_columnbank_order::execute($neworder); - $currentconfig = get_config('qbank_columnsortorder', 'enabledcol'); - $currentconfig = explode(',', $currentconfig); - $class = $currentconfig[array_rand($currentconfig, 1)]; - $randomplugintodisable = explode('\\', $class)[0]; - $olddisabledconfig = get_config('qbank_columnsortorder', 'disabledcol'); - $this->columnmanager->disable_columns($randomplugintodisable); - $newdisabledconfig = get_config('qbank_columnsortorder', 'disabledcol'); - $this->assertNotEquals($olddisabledconfig, $newdisabledconfig); - $this->columnmanager->enable_columns($randomplugintodisable); - $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); + 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)); } /** @@ -138,15 +267,21 @@ class column_manager_test extends advanced_testcase { * @covers \qbank_columnsortorder\event\plugin_observer */ public function test_plugin_enabled_disabled_observers(): void { - $neworder = $this->columnmanager->get_sorted_columns($this->columns); + $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); - set_columnbank_order::execute($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. - $class = $currentconfig[array_rand($currentconfig, 1)]; - $randomplugintodisable = explode('\\', $class)[0]; + $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'); @@ -159,4 +294,79 @@ class column_manager_test extends advanced_testcase { $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() { + $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); + } } diff --git a/question/bank/columnsortorder/tests/external/set_column_size_test.php b/question/bank/columnsortorder/tests/external/set_column_size_test.php new file mode 100644 index 00000000000..523e13b41c5 --- /dev/null +++ b/question/bank/columnsortorder/tests/external/set_column_size_test.php @@ -0,0 +1,67 @@ +. + +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 + * @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); + } +} diff --git a/question/bank/columnsortorder/tests/external/columnsortorder_external_test.php b/question/bank/columnsortorder/tests/external/set_columnbank_order_test.php similarity index 52% rename from question/bank/columnsortorder/tests/external/columnsortorder_external_test.php rename to question/bank/columnsortorder/tests/external/set_columnbank_order_test.php index 3c2020c07fc..c22fe3edd33 100644 --- a/question/bank/columnsortorder/tests/external/columnsortorder_external_test.php +++ b/question/bank/columnsortorder/tests/external/set_columnbank_order_test.php @@ -14,35 +14,41 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -namespace qbank_columnsortorder; +namespace qbank_columnsortorder\external; -use advanced_testcase; use qbank_columnsortorder\column_manager; -use qbank_columnsortorder\external\set_columnbank_order; +use qbank_columnsortorder\tests\external_function_testcase; -defined('MOODLE_INTERNAL') || die(); - -global $CFG; - -require_once($CFG->dirroot . '/question/classes/external.php'); +// 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 - * @author 2021, Ghaly Marc-Alexandre - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package qbank_columnsortorder + * @copyright 2023 Catalyst IT Europe Ltd. + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \qbank_columnsortorder\external\set_columnbank_order */ -class columnsortorder_external_test extends advanced_testcase { +class set_columnbank_order_test extends external_function_testcase { /** - * Test that external call core_question_external::set_columnbank_order($oldorder) sets proper - * data in config_plugins table. + * @var string Fully-qualified external function class to test. */ - public function test_columnorder_external(): void { - $this->resetAfterTest(true); - $this->setAdminUser(); + 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 = []; @@ -50,11 +56,6 @@ class columnsortorder_external_test extends advanced_testcase { $columnclasses[] = $columnnobject->class; } shuffle($columnclasses); - set_columnbank_order::execute($columnclasses); - - $currentconfig = (array)get_config('qbank_columnsortorder', 'enabledcol'); - $currentconfig = explode(',', $currentconfig[0]); - - $this->assertSame($columnclasses, $currentconfig); + return $columnclasses; } } diff --git a/question/bank/columnsortorder/tests/external/set_hidden_columns_test.php b/question/bank/columnsortorder/tests/external/set_hidden_columns_test.php new file mode 100644 index 00000000000..d38352a3aa3 --- /dev/null +++ b/question/bank/columnsortorder/tests/external/set_hidden_columns_test.php @@ -0,0 +1,56 @@ +. + +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 + * @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); + } +} diff --git a/question/bank/columnsortorder/version.php b/question/bank/columnsortorder/version.php index 168878b89b3..83861d9a9a3 100644 --- a/question/bank/columnsortorder/version.php +++ b/question/bank/columnsortorder/version.php @@ -26,6 +26,6 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'qbank_columnsortorder'; -$plugin->version = 2023042401; +$plugin->version = 2023052201; $plugin->requires = 2023041800; $plugin->maturity = MATURITY_STABLE; diff --git a/question/bank/comment/classes/comment_count_column.php b/question/bank/comment/classes/comment_count_column.php index 3995cf3367b..6fc542f5d3c 100644 --- a/question/bank/comment/classes/comment_count_column.php +++ b/question/bank/comment/classes/comment_count_column.php @@ -110,4 +110,7 @@ class comment_count_column extends column_base { return ['pr-3']; } + public function get_default_width(): int { + return 150; + } } diff --git a/question/bank/customfields/classes/custom_field_column.php b/question/bank/customfields/classes/custom_field_column.php index 469d6f9fceb..6a21e10b77e 100644 --- a/question/bank/customfields/classes/custom_field_column.php +++ b/question/bank/customfields/classes/custom_field_column.php @@ -17,6 +17,8 @@ namespace qbank_customfields; use core_question\local\bank\column_base; +use core_question\local\bank\view; +use qbank_customfields\customfield\question_handler; /** * A column type for the name of the question creator. @@ -42,6 +44,16 @@ class custom_field_column extends column_base { $this->field = $field; } + public static function from_column_name(view $view, string $columnname): custom_field_column { + $handler = question_handler::create(); + foreach ($handler->get_fields() as $field) { + if ($field->get('shortname') == $columnname) { + return new static($view, $field); + } + } + throw new \coding_exception('Custom field ' . $columnname . ' does not exist.'); + } + /** * Get the internal name for this column. Used as a CSS class name, * and to store information about the current sort. Must match PARAM_ALPHA. @@ -60,7 +72,7 @@ class custom_field_column extends column_base { * @return string The unique name; */ public function get_column_name(): string { - return 'custom_field_column\\' . $this->field->get('shortname'); + return $this->field->get('shortname'); } /** diff --git a/question/bank/customfields/tests/behat/customfield_question.feature b/question/bank/customfields/tests/behat/customfield_question.feature index 969f70085d4..75ecd4cc8d5 100644 --- a/question/bank/customfields/tests/behat/customfield_question.feature +++ b/question/bank/customfields/tests/behat/customfield_question.feature @@ -40,20 +40,21 @@ Feature: A teacher can edit question with custom fields And I am on the "Test quiz name" "mod_quiz > question bank" page Scenario: Edit a previously created question and see the custom field in the overview table and in the question preview. - When I choose "Edit question" action for "First question" in the question bank - And I should see "Category for test" - And I click on "Expand all" "link" in the "region-main" "region" + When I am on the "First question" "core_question > edit" page + Then I should see "Category for test" + And I expand all fieldsets And I should see "Field 1" And I set the following fields to these values: - | Field 1 | custom field text | + | Question name | First question edited | + | Field 1 | custom field text | And I press "id_submitbutton" And I should see "First question" And I should see "custom field text" - And I choose "Preview" action for "First question" in the question bank + And I am on the "First question edited" "core_question > preview" page And I should see "Field 1" - Then I should see "custom field text" + And I should see "custom field text" Scenario: Preview a previously created question with custom fields set with empty values - When I choose "Preview" action for "First question" in the question bank - And I should see "Field 1" - Then I should not see "custom field text" + When I am on the "First question" "core_question > preview" page + Then I should see "Field 1" + And I should not see "custom field text" diff --git a/question/bank/customfields/tests/behat/customfield_question_visbility.feature b/question/bank/customfields/tests/behat/customfield_question_visbility.feature index 8bcdab19b9a..fd3786ccdc1 100644 --- a/question/bank/customfields/tests/behat/customfield_question_visbility.feature +++ b/question/bank/customfields/tests/behat/customfield_question_visbility.feature @@ -37,13 +37,12 @@ Feature: The visibility of question custom fields control where they are display | Correct answer | False | | Feedback for the response 'True'. | So you think it is true | | Feedback for the response 'False'. | So you think it is false | - And I am on the "Test quiz name" "mod_quiz > question bank" page @javascript Scenario: Display custom question fields to teachers based on their visibility. - When I choose "Edit question" action for "First question" in the question bank - And I should see "Category for test" - And I click on "Expand all" "link" in the "region-main" "region" + When I am on the "First question" "core_question > edit" page + Then I should see "Category for test" + And I expand all fieldsets And I should see "Field 1" And I should see "Field 2" And I should see "Field 3" @@ -63,4 +62,4 @@ Feature: The visibility of question custom fields control where they are display And I should see "Field 2" And I should see "custom field text two" And I should see "Field 3" - Then I should see "secret" + And I should see "secret" diff --git a/question/bank/editquestion/classes/editquestion_helper.php b/question/bank/editquestion/classes/editquestion_helper.php index c806df5a9d1..77493bbc888 100644 --- a/question/bank/editquestion/classes/editquestion_helper.php +++ b/question/bank/editquestion/classes/editquestion_helper.php @@ -28,6 +28,7 @@ namespace qbank_editquestion; use core_question\local\bank\question_version_status; +use qbank_editquestion\output\add_new_question; /** * Class editquestion_helper for methods related to add/edit/copy @@ -69,25 +70,13 @@ class editquestion_helper { * @param bool $canadd the text to display on the button. * @param string $tooltip a tooltip to add to the button (optional). * @param bool $disabled if true, the button will be disabled. + * @deprecated since Moodle 4.3. Use {@see add_new_question} renderable instead + * @todo Final deprecation in Moodle 4.7 */ public static function create_new_question_button($categoryid, $params, $canadd, $tooltip = '', $disabled = false) { - global $PAGE, $OUTPUT; - - $addquestiondisplay = array(); - $addquestiondisplay['canadd'] = $canadd; - if ($canadd) { - $params['category'] = $categoryid; - $url = new \moodle_url('/question/bank/editquestion/addquestion.php', $params); - $buttonparams = ['disabled' => $disabled]; - if (!empty($tooltip)) { - $buttonparams['title'] = $tooltip; - } - $addquestiondisplay['buttonhtml'] = $OUTPUT->single_button($url, - get_string('createnewquestion', 'question'), - 'get', $buttonparams); - $addquestiondisplay['qtypeform'] = self::print_choose_qtype_to_add_form(array()); - } - return $PAGE->get_renderer('qbank_editquestion')->render_create_new_question_button($addquestiondisplay); + global $OUTPUT; + debugging('create_new_question_button() is deprecated. Use the add_new_question renderable instead.'); + return $OUTPUT->render(new add_new_question($categoryid, $params, $canadd)); } /** diff --git a/question/bank/editquestion/classes/output/add_new_question.php b/question/bank/editquestion/classes/output/add_new_question.php new file mode 100644 index 00000000000..60b97e8d9d3 --- /dev/null +++ b/question/bank/editquestion/classes/output/add_new_question.php @@ -0,0 +1,68 @@ +. + +namespace qbank_editquestion\output; + +use qbank_editquestion\editquestion_helper; +use renderer_base; + +/** + * Create new question button + * + * @package qbank_editquestion + * @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net} + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class add_new_question implements \renderable, \templatable { + /** @var int $categoryid The ID of the category the quesiton will be added to. */ + protected int $categoryid; + + /** @var array $params URL parameters to pass to the add question form. */ + protected array $params; + + /** @var bool $canadd True if the add question button should be displayed. If false, a placeholder will be shown.*/ + protected bool $canadd; + + /** + * Store data for building the template context. + * + * @param int $categoryid + * @param array $params + * @param bool $canadd + */ + public function __construct(int $categoryid, array $params, bool $canadd) { + $this->categoryid = $categoryid; + $this->params = $params; + $this->canadd = $canadd; + } + + public function export_for_template(renderer_base $output): array { + $addquestiondisplay = []; + $addquestiondisplay['canadd'] = $this->canadd; + if ($this->canadd) { + $this->params['category'] = $this->categoryid; + $url = new \moodle_url('/question/bank/editquestion/addquestion.php', $this->params); + $addquestiondisplay['buttonhtml'] = $output->single_button( + $url, + get_string('createnewquestion', 'question'), + 'get' + ); + $addquestiondisplay['qtypeform'] = editquestion_helper::print_choose_qtype_to_add_form([]); + } + return $addquestiondisplay; + } +} diff --git a/question/bank/editquestion/classes/output/renderer.php b/question/bank/editquestion/classes/output/renderer.php index 384135ee98d..b4fed061e08 100644 --- a/question/bank/editquestion/classes/output/renderer.php +++ b/question/bank/editquestion/classes/output/renderer.php @@ -52,8 +52,12 @@ class renderer extends \plugin_renderer_base { * * @param array $addquestiondata * @return bool|string + * @deprecated since Moodle 4.3. Use {@see add_new_question} renderable instead + * @todo Final deprecation in Moodle 4.7 */ public function render_create_new_question_button ($addquestiondata) { + debugging('render_create_new_question_button() is deprecated. ' + . 'Pass the add_new_question renderable to render() instead.'); return $this->render_from_template('qbank_editquestion/add_new_question', $addquestiondata); } diff --git a/question/bank/editquestion/classes/plugin_feature.php b/question/bank/editquestion/classes/plugin_feature.php index a2d6f316df9..bb5b813c28f 100644 --- a/question/bank/editquestion/classes/plugin_feature.php +++ b/question/bank/editquestion/classes/plugin_feature.php @@ -25,7 +25,9 @@ namespace qbank_editquestion; +use core\context; use core_question\local\bank\view; +use qbank_editquestion\output\add_new_question; /** * Class columns is the entrypoint for the columns. @@ -35,7 +37,7 @@ use core_question\local\bank\view; * @author Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class plugin_feature extends \core_question\local\bank\plugin_features_base{ +class plugin_feature extends \core_question\local\bank\plugin_features_base { public function get_question_columns($qbank): array { return [ @@ -50,4 +52,23 @@ class plugin_feature extends \core_question\local\bank\plugin_features_base{ ]; } + /** + * Return "Add new question" control. + * + * @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 { + if (!$qbank->allow_add_questions()) { + return []; + } + $canadd = has_capability('moodle/question:add', $context); + $urlparams = (new edit_action($qbank))->editquestionurl->params(); + return [ + 100 => new add_new_question($categoryid, $urlparams, $canadd), + ]; + } + } diff --git a/question/bank/history/classes/question_history_view.php b/question/bank/history/classes/question_history_view.php index 2e61cb0bcd1..33a91834fef 100644 --- a/question/bank/history/classes/question_history_view.php +++ b/question/bank/history/classes/question_history_view.php @@ -92,8 +92,9 @@ class question_history_view extends view { } } - protected function create_new_question_form($category, $canadd): void { + public function allow_add_questions(): bool { // As we dont want to create questions in this page. + return false; } /** diff --git a/question/bank/statistics/classes/columns/discrimination_index.php b/question/bank/statistics/classes/columns/discrimination_index.php index 054e53851e5..d5c769adc44 100644 --- a/question/bank/statistics/classes/columns/discrimination_index.php +++ b/question/bank/statistics/classes/columns/discrimination_index.php @@ -53,6 +53,14 @@ class discrimination_index extends column_base { echo $PAGE->get_renderer('qbank_statistics')->render_discrimination_index($discriminationindex); } + public function display_preview(\stdClass $question, string $rowclasses): void { + global $PAGE; + + $this->display_start($question, $rowclasses); + echo $PAGE->get_renderer('qbank_statistics')->render_discrimination_index(50.00); + $this->display_end($question, $rowclasses);; + } + public function get_extra_classes(): array { return ['pr-3']; } diff --git a/question/bank/statistics/classes/columns/discriminative_efficiency.php b/question/bank/statistics/classes/columns/discriminative_efficiency.php index 82dd400e9fd..c2b66300d27 100644 --- a/question/bank/statistics/classes/columns/discriminative_efficiency.php +++ b/question/bank/statistics/classes/columns/discriminative_efficiency.php @@ -51,6 +51,14 @@ class discriminative_efficiency extends column_base { echo $PAGE->get_renderer('qbank_statistics')->render_discriminative_efficiency($discriminativeefficiency); } + public function display_preview(\stdClass $question, string $rowclasses): void { + global $PAGE; + + $this->display_start($question, $rowclasses); + echo $PAGE->get_renderer('qbank_statistics')->render_discriminative_efficiency(25); + $this->display_end($question, $rowclasses);; + } + public function get_extra_classes(): array { return ['pr-3']; } diff --git a/question/bank/statistics/classes/columns/facility_index.php b/question/bank/statistics/classes/columns/facility_index.php index cae02d81df5..b04c0d639f1 100644 --- a/question/bank/statistics/classes/columns/facility_index.php +++ b/question/bank/statistics/classes/columns/facility_index.php @@ -52,6 +52,14 @@ class facility_index extends column_base { echo $PAGE->get_renderer('qbank_statistics')->render_facility_index($facility); } + public function display_preview(\stdClass $question, string $rowclasses): void { + global $PAGE; + + $this->display_start($question, $rowclasses); + echo $PAGE->get_renderer('qbank_statistics')->render_facility_index(0.75); + $this->display_end($question, $rowclasses);; + } + public function get_extra_classes(): array { return ['pr-3']; } diff --git a/question/bank/statistics/tests/behat/statistics_values.feature b/question/bank/statistics/tests/behat/statistics_values.feature index d14f3cd370e..4832e44a604 100644 --- a/question/bank/statistics/tests/behat/statistics_values.feature +++ b/question/bank/statistics/tests/behat/statistics_values.feature @@ -210,10 +210,8 @@ Feature: Show statistics in question bank And I run the scheduled task "\quiz_statistics\task\recalculate" # Confirm the "Needs checking?" column matches the expected values based on students' answers When I am on the "Quiz 3" "mod_quiz > question bank" page logged in as "admin" - Then the following should exist in the "categoryquestions" table: - | Question | Needs checking? | - | MCA | Likely | - | MCB | Very likely | - | MCC | Unlikely | - | MCD | Likely | - | MCE | Very likely | + Then "Likely" "text" should exist in the "MCA" "table_row" + And "Very likely" "text" should exist in the "MCB" "table_row" + And "Unlikely" "text" should exist in the "MCC" "table_row" + And "Likely" "text" should exist in the "MCD" "table_row" + And "Very likely" "text" should exist in the "MCE" "table_row" diff --git a/question/bank/usage/styles.css b/question/bank/usage/styles.css index 3f9e0ea49a2..4d928f1055b 100644 --- a/question/bank/usage/styles.css +++ b/question/bank/usage/styles.css @@ -1,4 +1,4 @@ -#categoryquestions td.questionlastused span.date { +.question-bank-table td.questionlastused span.date { font-weight: 400; font-size: .8em; } diff --git a/question/bank/viewquestionname/classes/question_name_idnumber_tags_column.php b/question/bank/viewquestionname/classes/question_name_idnumber_tags_column.php index 354755bc30a..c21c5a6dfe0 100644 --- a/question/bank/viewquestionname/classes/question_name_idnumber_tags_column.php +++ b/question/bank/viewquestionname/classes/question_name_idnumber_tags_column.php @@ -50,8 +50,8 @@ class question_name_idnumber_tags_column extends viewquestionname_column_helper // The non-breaking space ' ' is used in html to fix MDL-75051 (browser issues caused by chrome and Edge). if ($question->idnumber !== null && $question->idnumber !== '') { echo ' ' . \html_writer::span( - \html_writer::span(get_string('idnumber', 'question'), 'accesshide') - . ' ' . \html_writer::span(s($question->idnumber), 'badge badge-primary'), 'ml-1'); + \html_writer::span(get_string('idnumber', 'question') . ' ', 'accesshide') + . \html_writer::span(s($question->idnumber), 'badge badge-primary'), 'ml-1'); } // Question tags. diff --git a/question/bank/viewquestionname/classes/viewquestionname_column_helper.php b/question/bank/viewquestionname/classes/viewquestionname_column_helper.php index 6e3027aa4bb..96d1a171ba5 100644 --- a/question/bank/viewquestionname/classes/viewquestionname_column_helper.php +++ b/question/bank/viewquestionname/classes/viewquestionname_column_helper.php @@ -72,4 +72,7 @@ class viewquestionname_column_helper extends column_base { return 'q.name'; } + public function get_default_width(): int { + return 250; + } } diff --git a/question/bank/viewquestiontext/amd/build/datafilter/filtertypes/showtext.min.js b/question/bank/viewquestiontext/amd/build/datafilter/filtertypes/showtext.min.js deleted file mode 100644 index 538127539f6..00000000000 --- a/question/bank/viewquestiontext/amd/build/datafilter/filtertypes/showtext.min.js +++ /dev/null @@ -1,11 +0,0 @@ -define("qbank_viewquestiontext/datafilter/filtertypes/showtext",["exports","core/datafilter/filtertype"],(function(_exports,_filtertype){var obj; -/** - * Filter managing question text display. - * - * @module qbank_viewquestiontext/datafilter/filtertypes/showtext - * @author 2022 Ghaly Marc-Alexandre - * @copyright 2022 Catalyst IT Australia Pty Ltd - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_filtertype=(obj=_filtertype)&&obj.__esModule?obj:{default:obj};class _default extends _filtertype.default{}return _exports.default=_default,_exports.default})); - -//# sourceMappingURL=showtext.min.js.map \ No newline at end of file diff --git a/question/bank/viewquestiontext/amd/build/datafilter/filtertypes/showtext.min.js.map b/question/bank/viewquestiontext/amd/build/datafilter/filtertypes/showtext.min.js.map deleted file mode 100644 index 719c279276c..00000000000 --- a/question/bank/viewquestiontext/amd/build/datafilter/filtertypes/showtext.min.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"showtext.min.js","sources":["../../../src/datafilter/filtertypes/showtext.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Filter managing question text display.\n *\n * @module qbank_viewquestiontext/datafilter/filtertypes/showtext\n * @author 2022 Ghaly Marc-Alexandre \n * @copyright 2022 Catalyst IT Australia Pty Ltd\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport GenericFilter from 'core/datafilter/filtertype';\n\nexport default class extends GenericFilter {\n}\n"],"names":["GenericFilter"],"mappings":";;;;;;;;4KA0B6BA"} \ No newline at end of file diff --git a/question/bank/viewquestiontext/amd/build/question_text_format.min.js b/question/bank/viewquestiontext/amd/build/question_text_format.min.js new file mode 100644 index 00000000000..b06dd1c6616 --- /dev/null +++ b/question/bank/viewquestiontext/amd/build/question_text_format.min.js @@ -0,0 +1,11 @@ +define("qbank_viewquestiontext/question_text_format",["exports","qbank_viewquestiontext/repository","core_question/refresh_ui","core/notification"],(function(_exports,repository,_refresh_ui,_notification){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)}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,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} +/** + * Javascript for question_text_format question bank control. + * + * @module qbank_viewquestiontext/question_text_format + * @copyright 2023 Catalyst IT Europe Ltd. + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */(repository),_refresh_ui=_interopRequireDefault(_refresh_ui),_notification=_interopRequireDefault(_notification);const SELECTORS_formatSelectId="question-text-format",SELECTORS_returnUrl="[name=returnurl]";let uiRoot;const handleFormatChange=async e=>{const value=e.target.value;try{await repository.setQuestionTextFormat(value);const returnUrlInput=e.target.closest("form").querySelector(SELECTORS_returnUrl),returnUrl=new URL(returnUrlInput.value);await _refresh_ui.default.refresh(uiRoot,returnUrl)}catch(ex){_notification.default.exception(ex)}};_exports.init=uiRootId=>{uiRoot=document.getElementById(uiRootId);document.getElementById(SELECTORS_formatSelectId).addEventListener("change",handleFormatChange)}})); + +//# sourceMappingURL=question_text_format.min.js.map \ No newline at end of file diff --git a/question/bank/viewquestiontext/amd/build/question_text_format.min.js.map b/question/bank/viewquestiontext/amd/build/question_text_format.min.js.map new file mode 100644 index 00000000000..a80fb9b47f7 --- /dev/null +++ b/question/bank/viewquestiontext/amd/build/question_text_format.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"question_text_format.min.js","sources":["../src/question_text_format.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Javascript for question_text_format question bank control.\n *\n * @module qbank_viewquestiontext/question_text_format\n * @copyright 2023 Catalyst IT Europe Ltd.\n * @author Mark Johnson \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport * as repository from 'qbank_viewquestiontext/repository';\nimport RefreshUi from 'core_question/refresh_ui';\nimport Notification from 'core/notification';\n\nconst SELECTORS = {\n formatSelectId: 'question-text-format',\n returnUrl: '[name=returnurl]',\n};\n\nlet uiRoot;\n\n/**\n * Save the selected format via a web service call, and refresh the UI.\n *\n * @param {Event} e Select field change event.\n * @return {Promise}\n */\nconst handleFormatChange = async(e) => {\n const value = e.target.value;\n try {\n await repository.setQuestionTextFormat(value);\n const returnUrlInput = e.target.closest('form').querySelector(SELECTORS.returnUrl);\n const returnUrl = new URL(returnUrlInput.value);\n await RefreshUi.refresh(uiRoot, returnUrl);\n } catch (ex) {\n Notification.exception(ex);\n }\n};\n\n/**\n * Initialise question text format widget.\n *\n * Find the uiRoot element and attach a change listener to the question text format selector.\n *\n * @param {String} uiRootId\n */\nexport const init = (uiRootId) => {\n uiRoot = document.getElementById(uiRootId);\n const select = document.getElementById(SELECTORS.formatSelectId);\n select.addEventListener('change', handleFormatChange);\n};\n"],"names":["SELECTORS","uiRoot","handleFormatChange","async","value","e","target","repository","setQuestionTextFormat","returnUrlInput","closest","querySelector","returnUrl","URL","RefreshUi","refresh","ex","exception","uiRootId","document","getElementById","addEventListener"],"mappings":";;;;;;;;4HA4BMA,yBACc,uBADdA,oBAES,uBAGXC,aAQEC,mBAAqBC,MAAAA,UACjBC,MAAQC,EAAEC,OAAOF,gBAEbG,WAAWC,sBAAsBJ,aACjCK,eAAiBJ,EAAEC,OAAOI,QAAQ,QAAQC,cAAcX,qBACxDY,UAAY,IAAIC,IAAIJ,eAAeL,aACnCU,oBAAUC,QAAQd,OAAQW,WAClC,MAAOI,0BACQC,UAAUD,oBAWVE,WACjBjB,OAASkB,SAASC,eAAeF,UAClBC,SAASC,eAAepB,0BAChCqB,iBAAiB,SAAUnB"} \ No newline at end of file diff --git a/question/bank/viewquestiontext/amd/build/repository.min.js b/question/bank/viewquestiontext/amd/build/repository.min.js new file mode 100644 index 00000000000..bc5abaabdf3 --- /dev/null +++ b/question/bank/viewquestiontext/amd/build/repository.min.js @@ -0,0 +1,3 @@ +define("qbank_viewquestiontext/repository",["exports","core/ajax"],(function(_exports,_ajax){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.setQuestionTextFormat=void 0;_exports.setQuestionTextFormat=format=>(0,_ajax.call)([{methodname:"qbank_viewquestiontext_set_question_text_format",args:{format:format}}])[0]})); + +//# sourceMappingURL=repository.min.js.map \ No newline at end of file diff --git a/question/bank/viewquestiontext/amd/build/repository.min.js.map b/question/bank/viewquestiontext/amd/build/repository.min.js.map new file mode 100644 index 00000000000..7b92e1ffab8 --- /dev/null +++ b/question/bank/viewquestiontext/amd/build/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 .\n\n/**\n * External function calls for qbank_columnsortorder\n *\n * @module qbank_viewquestiontext/repository\n * @copyright 2023 Catalyst IT Europe Ltd.\n * @author Mark Johnson \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 * Set the question text format.\n *\n * @param {Number} format The question text format.\n * @return {Promise}\n */\nexport const setQuestionTextFormat = (format) => fetchMany([{\n methodname: 'qbank_viewquestiontext_set_question_text_format',\n args: {\n format,\n },\n}])[0];\n"],"names":["format","methodname","args"],"mappings":"0NAgCsCA,SAAW,cAAU,CAAC,CACxDC,WAAY,kDACZC,KAAM,CACFF,OAAAA,WAEJ"} \ No newline at end of file diff --git a/question/bank/viewquestiontext/amd/src/question_text_format.js b/question/bank/viewquestiontext/amd/src/question_text_format.js new file mode 100644 index 00000000000..2186d9d69f8 --- /dev/null +++ b/question/bank/viewquestiontext/amd/src/question_text_format.js @@ -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 . + +/** + * Javascript for question_text_format question bank control. + * + * @module qbank_viewquestiontext/question_text_format + * @copyright 2023 Catalyst IT Europe Ltd. + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import * as repository from 'qbank_viewquestiontext/repository'; +import RefreshUi from 'core_question/refresh_ui'; +import Notification from 'core/notification'; + +const SELECTORS = { + formatSelectId: 'question-text-format', + returnUrl: '[name=returnurl]', +}; + +let uiRoot; + +/** + * Save the selected format via a web service call, and refresh the UI. + * + * @param {Event} e Select field change event. + * @return {Promise} + */ +const handleFormatChange = async(e) => { + const value = e.target.value; + try { + await repository.setQuestionTextFormat(value); + const returnUrlInput = e.target.closest('form').querySelector(SELECTORS.returnUrl); + const returnUrl = new URL(returnUrlInput.value); + await RefreshUi.refresh(uiRoot, returnUrl); + } catch (ex) { + Notification.exception(ex); + } +}; + +/** + * Initialise question text format widget. + * + * Find the uiRoot element and attach a change listener to the question text format selector. + * + * @param {String} uiRootId + */ +export const init = (uiRootId) => { + uiRoot = document.getElementById(uiRootId); + const select = document.getElementById(SELECTORS.formatSelectId); + select.addEventListener('change', handleFormatChange); +}; diff --git a/question/bank/viewquestiontext/amd/src/datafilter/filtertypes/showtext.js b/question/bank/viewquestiontext/amd/src/repository.js similarity index 58% rename from question/bank/viewquestiontext/amd/src/datafilter/filtertypes/showtext.js rename to question/bank/viewquestiontext/amd/src/repository.js index 5a88b46cabd..e7e0b64bbdb 100644 --- a/question/bank/viewquestiontext/amd/src/datafilter/filtertypes/showtext.js +++ b/question/bank/viewquestiontext/amd/src/repository.js @@ -14,15 +14,25 @@ // along with Moodle. If not, see . /** - * Filter managing question text display. + * External function calls for qbank_columnsortorder * - * @module qbank_viewquestiontext/datafilter/filtertypes/showtext - * @author 2022 Ghaly Marc-Alexandre - * @copyright 2022 Catalyst IT Australia Pty Ltd + * @module qbank_viewquestiontext/repository + * @copyright 2023 Catalyst IT Europe Ltd. + * @author Mark Johnson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -import GenericFilter from 'core/datafilter/filtertype'; +import {call as fetchMany} from 'core/ajax'; -export default class extends GenericFilter { -} +/** + * Set the question text format. + * + * @param {Number} format The question text format. + * @return {Promise} + */ +export const setQuestionTextFormat = (format) => fetchMany([{ + methodname: 'qbank_viewquestiontext_set_question_text_format', + args: { + format, + }, +}])[0]; diff --git a/question/bank/viewquestiontext/classes/external/set_question_text_format.php b/question/bank/viewquestiontext/classes/external/set_question_text_format.php new file mode 100644 index 00000000000..4e988c9a347 --- /dev/null +++ b/question/bank/viewquestiontext/classes/external/set_question_text_format.php @@ -0,0 +1,79 @@ +. + +namespace qbank_viewquestiontext\external; + +use context_system; +use core_external\external_api; +use core_external\external_function_parameters; +use core_external\external_value; +use qbank_viewquestiontext\output\question_text_format; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/question/editlib.php'); + +/** + * External function for setting the question text format. + * + * @package qbank_viewquestiontext + * @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net} + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class set_question_text_format 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([ + 'format' => new external_value(PARAM_INT, 'Format for the question text', VALUE_REQUIRED), + ]); + } + + /** + * Returns description of method result value. + */ + public static function execute_returns(): void { + } + + /** + * Save the question text format preference for the current user. + * + * @param int $format Format for the question text. + */ + public static function execute(int $format): void { + [ + 'format' => $format, + ] = self::validate_parameters( + self::execute_parameters(), + [ + 'format' => $format, + ] + ); + + if (!in_array($format, [question_text_format::OFF, question_text_format::PLAIN, question_text_format::FULL])) { + throw new \invalid_parameter_exception('$format must be one of question_text_format::OFF, ::PLAIN or ::FULL.'); + } + + $context = context_system::instance(); + self::validate_context($context); + + \question_set_or_get_user_preference('qbshowtext', $format, 0, new \moodle_url('/')); + } +} diff --git a/question/bank/viewquestiontext/classes/output/question_text_format.php b/question/bank/viewquestiontext/classes/output/question_text_format.php new file mode 100644 index 00000000000..aa6f8edfaf9 --- /dev/null +++ b/question/bank/viewquestiontext/classes/output/question_text_format.php @@ -0,0 +1,92 @@ +. + +namespace qbank_viewquestiontext\output; + +use core_question\local\bank\view; +use qbank_viewquestiontext\question_text_row; +use renderer_base; + +/** + * Question text format selector. + * + * @package qbank_viewquestiontext + * @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net} + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class question_text_format implements \renderable, \templatable { + /** + * @var int Question text is off. + */ + const OFF = 0; + + /** + * @var int Question text is displayed in plain text mode. + */ + const PLAIN = 1; + + /** + * @var int Question text is displayed fully rendered. + */ + const FULL = 2; + + /** @var int|mixed The current display preference value. */ + protected int $preference; + + /** + * @var \moodle_url The return URL for redirecting back to the current question bank page. + */ + protected \moodle_url $returnurl; + + /** + * Store the returnurl and the current preference value. + * + * @param view $qbank + * @throws \moodle_exception + */ + public function __construct(view $qbank) { + $row = new question_text_row($qbank); + $this->returnurl = new \moodle_url($qbank->returnurl); + $this->preference = question_get_display_preference($row->get_preference_key(), 0, PARAM_INT, new \moodle_url('')); + } + + public function export_for_template(renderer_base $output): array { + return [ + 'formaction' => new \moodle_url('/question/bank/viewquestiontext/save.php'), + 'sesskey' => sesskey(), + 'returnurl' => $this->returnurl->out(false), + 'options' => [ + (object)[ + 'label' => get_string('showquestiontext_off', 'question'), + 'value' => self::OFF, + 'selected' => $this->preference === self::OFF, + ], + (object)[ + 'label' => get_string('showquestiontext_plain', 'question'), + 'value' => self::PLAIN, + 'selected' => $this->preference === self::PLAIN, + ], + (object)[ + 'label' => get_string('showquestiontext_full', 'question'), + 'value' => self::FULL, + 'selected' => $this->preference === self::FULL, + ], + ], + 'label' => get_string('showquestiontext', 'core_question'), + ]; + } +} diff --git a/question/bank/viewquestiontext/classes/plugin_feature.php b/question/bank/viewquestiontext/classes/plugin_feature.php index 2b7ac9c87f2..ca78b018ba9 100644 --- a/question/bank/viewquestiontext/classes/plugin_feature.php +++ b/question/bank/viewquestiontext/classes/plugin_feature.php @@ -16,8 +16,10 @@ namespace qbank_viewquestiontext; +use core\context; use core_question\local\bank\plugin_features_base; use core_question\local\bank\view; +use qbank_viewquestiontext\output\question_text_format; /** * Class columns is the entrypoint for the columns. @@ -29,15 +31,27 @@ use core_question\local\bank\view; */ class plugin_feature extends plugin_features_base { - public function get_question_columns($qbank): array { - return [ - new question_text_row($qbank) - ]; + /** + * Return an additional row for displaying the question text, if user has a preference set to display it. + * + * @param view $qbank + * @return array + */ + public function get_question_columns(view $qbank): array { + $row = new question_text_row($qbank); + $preference = (int)question_get_display_preference($row->get_preference_key(), '0', PARAM_INT, new \moodle_url('/')); + if ($preference != question_text_format::OFF) { + return [ + $row, + ]; + } + return []; + } - public function get_question_filters(view $qbank = null): array { + public function get_question_bank_controls(view $qbank, context $context, int $categoryid): array { return [ - new questiontext_condition($qbank), + 400 => new question_text_format($qbank), ]; } } diff --git a/question/bank/viewquestiontext/classes/question_text_row.php b/question/bank/viewquestiontext/classes/question_text_row.php index 98dbc9e2ee4..998c777f275 100644 --- a/question/bank/viewquestiontext/classes/question_text_row.php +++ b/question/bank/viewquestiontext/classes/question_text_row.php @@ -17,6 +17,7 @@ namespace qbank_viewquestiontext; use core_question\local\bank\row_base; +use qbank_viewquestiontext\output\question_text_format; use question_utils; /** @@ -29,8 +30,8 @@ use question_utils; */ class question_text_row extends row_base { - /** @var bool if true, we will show the question text reduced to plain text, else it is fully rendered. */ - protected $plain; + /** @var int if true, we will show the question text reduced to plain text, else it is fully rendered. */ + protected $preference; /** @var \stdClass $formatoptions options used when displaying the question text as HTML. */ protected $formatoptions; @@ -38,8 +39,7 @@ class question_text_row extends row_base { protected function init(): void { // Cannot use $this->get_preference because of PHP type hints. - $preference = question_get_display_preference($this->get_preference_key(), 0, PARAM_INT, new \moodle_url('')); - $this->plain = 1 === (int) $preference; + $this->preference = (int)question_get_display_preference($this->get_preference_key(), 0, PARAM_INT, new \moodle_url('')); $this->formatoptions = new \stdClass(); $this->formatoptions->noclean = true; $this->formatoptions->para = false; @@ -55,14 +55,12 @@ class question_text_row extends row_base { protected function display_content($question, $rowclasses): void { // Access 'showtext' filter from pagevars. - $display = $this->qbank->get_pagevars('filter')['showtext'] ?? null; - if ($display) { - $showtext = (int)$display['values'][0]; + if ($this->preference !== question_text_format::OFF) { $text = ''; - if ($showtext === questiontext_condition::PLAIN) { + if ($this->preference === question_text_format::PLAIN) { $text = question_utils::to_plain_text($question->questiontext, $question->questiontextformat, ['noclean' => true, 'para' => false, 'filter' => false]); - } else if ($showtext === questiontext_condition::FULL) { + } else if ($this->preference === question_text_format::FULL) { $text = question_rewrite_question_preview_urls($question->questiontext, $question->id, $question->contextid, 'question', 'questiontext', $question->id, $question->contextid, 'core_question'); diff --git a/question/bank/viewquestiontext/classes/questiontext_condition.php b/question/bank/viewquestiontext/classes/questiontext_condition.php deleted file mode 100644 index 6d1d0df111e..00000000000 --- a/question/bank/viewquestiontext/classes/questiontext_condition.php +++ /dev/null @@ -1,108 +0,0 @@ -. - -namespace qbank_viewquestiontext; - -use core\output\datafilter; -use core_question\local\bank\condition; -use core_question\local\bank\view; - -/** - * This class controls from which category questions are listed. - * - * @package qbank_viewquestiontext - * @copyright 2022 Catalyst IT Australia Pty Ltd - * @author Ghaly Marc-Alexandre - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class questiontext_condition extends condition { - - /** - * @var int Question text is off. - */ - const OFF = 0; - - /** - * @var int Question text is displayed in plain text mode. - */ - const PLAIN = 1; - - /** - * @var int Question text is displayed fully rendered. - */ - const FULL = 2; - - /** - * @var ?int $showtext Initial value of the filter, for determining the pre-selected option. - */ - protected ?int $showtext = null; - - public function __construct(view $qbank = null) { - if (is_null($qbank)) { - return; - } - $filter = $qbank->get_pagevars('filter'); - if (isset($filter['showtext'])) { - $this->showtext = (int)$filter['showtext']['values'][0]; - } - } - - public function allow_custom() { - return false; - } - - public function allow_multiple() { - return false; - } - - public static function get_condition_key() { - return 'showtext'; - } - - public function get_title() { - return get_string('showquestiontext', 'core_question'); - } - - public function get_join_list(): array { - return [ - datafilter::JOINTYPE_ANY, - ]; - } - - public function get_filter_class() { - return 'qbank_viewquestiontext/datafilter/filtertypes/showtext'; - } - - public function get_initial_values() { - return [ - [ - 'value' => self::OFF, - 'title' => get_string('showquestiontext_off', 'question'), - 'selected' => $this->showtext === self::OFF, - ], - [ - 'value' => self::PLAIN, - 'title' => get_string('showquestiontext_plain', 'question'), - 'selected' => $this->showtext === self::PLAIN, - ], - [ - 'value' => self::FULL, - 'title' => get_string('showquestiontext_full', 'question'), - 'selected' => $this->showtext === self::FULL, - ] - ]; - } -} diff --git a/question/bank/viewquestiontext/db/services.php b/question/bank/viewquestiontext/db/services.php new file mode 100644 index 00000000000..224d0bc6546 --- /dev/null +++ b/question/bank/viewquestiontext/db/services.php @@ -0,0 +1,36 @@ +. + +/** + * qbank_viewquestiontext external functions and service definitions. + * + * @package qbank_viewquestiontext + * @category webservice + * @copyright 2023 Catalyst IT Europe Ltd + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$functions = [ + 'qbank_viewquestiontext_set_question_text_format' => [ + 'classname' => 'qbank_viewquestiontext\external\set_question_text_format', + 'description' => 'Sets the preference for displaying and formatting the question text', + 'type' => 'write', + 'ajax' => true, + ], +]; diff --git a/question/bank/viewquestiontext/save.php b/question/bank/viewquestiontext/save.php new file mode 100644 index 00000000000..5c14c1a2f72 --- /dev/null +++ b/question/bank/viewquestiontext/save.php @@ -0,0 +1,48 @@ +. + +/** + * Synchronously save the question text display preference, and redirect back to the previous page. + * + * This is progressively enhanced by question_text_format.js, but this remains as a fallback. + * + * @package qbank_viewquestiontext + * @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net} + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../../../config.php'); +require_once($CFG->dirroot . '/question/editlib.php'); + +$format = required_param('format', PARAM_INT); +$returnurl = required_param('returnurl', PARAM_LOCALURL); + +require_login(); +require_sesskey(); + +$validformats = [ + \qbank_viewquestiontext\output\question_text_format::OFF, + \qbank_viewquestiontext\output\question_text_format::PLAIN, + \qbank_viewquestiontext\output\question_text_format::FULL, +]; +if (!in_array($format, $validformats)) { + throw new \invalid_parameter_exception('$format must be one of question_text_format::OFF, ::PLAIN or ::FULL.'); +} + +question_set_or_get_user_preference('qbshowtext', $format, 0, new \moodle_url('/')); + +redirect(new moodle_url($returnurl)); diff --git a/question/bank/viewquestiontext/styles.css b/question/bank/viewquestiontext/styles.css new file mode 100644 index 00000000000..d35b2f11df0 --- /dev/null +++ b/question/bank/viewquestiontext/styles.css @@ -0,0 +1,3 @@ +.jsenabled .question-text-format .input-group-append { + display: none; +} diff --git a/question/bank/viewquestiontext/templates/question_text_format.mustache b/question/bank/viewquestiontext/templates/question_text_format.mustache new file mode 100644 index 00000000000..c240bffb12b --- /dev/null +++ b/question/bank/viewquestiontext/templates/question_text_format.mustache @@ -0,0 +1,58 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template qbank_viewquestiontext/question_text_format + + Example context (json): + { + "label": "Question text format", + "options": [ + { + "value": 1, + "label": "Plain text", + "selected": true + }, + { + "value": 2, + "label": "Fully rendered", + "selected": false + } + ] + } +}} +
+ + +
+ + +
+ +
+
+
+{{#js}} + require(['qbank_viewquestiontext/question_text_format'], QuestionTextFormat => { + QuestionTextFormat.init('questionscontainer'); + }); +{{/js}} \ No newline at end of file diff --git a/question/bank/viewquestiontext/tests/behat/view_question_text_column.feature b/question/bank/viewquestiontext/tests/behat/view_question_text_column.feature index a2829f2adec..995598f78bf 100644 --- a/question/bank/viewquestiontext/tests/behat/view_question_text_column.feature +++ b/question/bank/viewquestiontext/tests/behat/view_question_text_column.feature @@ -19,19 +19,19 @@ Feature: Use the qbank plugin manager page for viewquestiontext @javascript Scenario: Display of plain question text can be turned on and off When I am on the "Test quiz" "mod_quiz > question bank" page logged in as admin - And I apply question bank filter "Show question text in the question list?" with value "text only" + And I set the field "Show question text in the question list?" to "text only" Then I should see "Answer the first question" And ".totestforhtml" "css_element" should not exist in the "Answer the first question" "table_row" - And I apply question bank filter "Show question text in the question list?" with value "No" + And I set the field "Show question text in the question list?" to "No" And I should not see "Answer the first question" @javascript Scenario: Display of full question text can be turned on and off When I am on the "Test quiz" "mod_quiz > question bank" page logged in as admin - And I apply question bank filter "Show question text in the question list?" with value "with images" + And I set the field "Show question text in the question list?" to "with images" Then I should see "Answer the first question" And ".totestforhtml" "css_element" should exist in the "Answer the first question" "table_row" - And I apply question bank filter "Show question text in the question list?" with value "No" + And I set the field "Show question text in the question list?" to "No" And I should not see "Answer the first question" @javascript @@ -53,5 +53,5 @@ Feature: Use the qbank plugin manager page for viewquestiontext And I navigate to "Plugins > Question bank plugins > Manage question bank plugins" in site administration And I click on "Enable" "link" in the "View question text" "table_row" And I am on the "Test quiz" "mod_quiz > question bank" page - When I apply question bank filter "Show question text in the question list?" with value "text only" + When I set the field "Show question text in the question list?" to "text only" And I should see "Answer the first question" diff --git a/question/bank/viewquestiontext/version.php b/question/bank/viewquestiontext/version.php index b9a8245fae2..a4771b5e428 100644 --- a/question/bank/viewquestiontext/version.php +++ b/question/bank/viewquestiontext/version.php @@ -26,6 +26,6 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'qbank_viewquestiontext'; -$plugin->version = 2023042400; +$plugin->version = 2023042401; $plugin->requires = 2023041800; $plugin->maturity = MATURITY_STABLE; diff --git a/question/bank/viewquestiontype/classes/question_type_column.php b/question/bank/viewquestiontype/classes/question_type_column.php index bffc58f2b19..ed81ea66c69 100644 --- a/question/bank/viewquestiontype/classes/question_type_column.php +++ b/question/bank/viewquestiontype/classes/question_type_column.php @@ -52,4 +52,7 @@ class question_type_column extends column_base { return 'q.qtype'; } + public function get_default_width(): int { + return 45; + } } diff --git a/question/classes/local/bank/checkbox_column.php b/question/classes/local/bank/checkbox_column.php index 938d8864cc4..1b022f118be 100644 --- a/question/classes/local/bank/checkbox_column.php +++ b/question/classes/local/bank/checkbox_column.php @@ -57,6 +57,27 @@ class checkbox_column extends column_base { return get_string('selectquestionsforbulk', 'question'); } + public function display_header(array $columnactions = [], string $width = ''): void { + global $PAGE; + $renderer = $PAGE->get_renderer('core_question', 'bank'); + + $data = []; + $data['sortable'] = false; + $data['extraclasses'] = $this->get_classes(); + $name = get_class($this); + $data['sorttip'] = true; + $data['tiptitle'] = $this->get_title(); + $data['tip'] = $this->get_title_tip(); + + $data['colname'] = $this->get_column_name(); + $data['columnid'] = $this->get_column_id(); + $data['name'] = get_string('selectall'); + $data['class'] = $name; + $data['width'] = $width; + + echo $renderer->render_column_header($data); + } + protected function display_content($question, $rowclasses): void { global $OUTPUT; @@ -75,4 +96,7 @@ class checkbox_column extends column_base { return ['q.id']; } + public function get_default_width(): int { + return 25; + } } diff --git a/question/classes/local/bank/column_action_base.php b/question/classes/local/bank/column_action_base.php new file mode 100644 index 00000000000..5c7060ff582 --- /dev/null +++ b/question/classes/local/bank/column_action_base.php @@ -0,0 +1,46 @@ +. + +namespace core_question\local\bank; + +/** + * Base class to implement actions that can be performed on any column. + * + * A plugin should define subclasses of this for each action it provides, and return an instance of each from + * plugin_feature::get_column_actions(). The action returned from {@see get_action_menu_link()} will be displayed in each column + * header. + * + * @package core_question + * @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net} + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class column_action_base extends view_component { + /** + * A chance for subclasses to initialise themselves, for example to load lang strings, + * without having to override the constructor. + */ + protected function init(): void { + } + + /** + * Return the action menu link for this action on the supplied column. + * + * @param column_base $column The column we are providing the action for. + * @return ?\action_menu_link The action to display in the column header. + */ + abstract public function get_action_menu_link(column_base $column): ?\action_menu_link; +} diff --git a/question/classes/local/bank/column_base.php b/question/classes/local/bank/column_base.php index 2875d918762..cc122865de3 100644 --- a/question/classes/local/bank/column_base.php +++ b/question/classes/local/bank/column_base.php @@ -33,6 +33,11 @@ namespace core_question\local\bank; */ abstract class column_base extends view_component { + /** + * @var string A separator for joining column attributes together into a unique ID string. + */ + const ID_SEPARATOR = '-'; + /** * @var view $qbank the question bank view we are helping to render. */ @@ -41,6 +46,23 @@ abstract class column_base extends view_component { /** @var bool determine whether the column is td or th. */ protected $isheading = false; + /** @var bool determine whether the column is visible */ + public $isvisible = true; + + /** + * Return an instance of this column, based on the column name. + * + * In the case of the base class, we don't actually use the column name since the class represents one specific column. + * However, sub-classes may use the column name as an additional constructor to the parameter. + * + * @param view $view Question bank view + * @param string $columnname The column name for this instance, as returned by {@see get_column_name()} + * @return column_base An instance of this class. + */ + public static function from_column_name(view $view, string $columnname): column_base { + return new static($view); + } + /** * Set the column as heading */ @@ -78,8 +100,11 @@ abstract class column_base extends view_component { /** * Output the column header cell. + * + * @param column_action_base[] $columnactions A list of column actions to include in the header. + * @param string $width A CSS width property value. */ - public function display_header(): void { + public function display_header(array $columnactions = [], string $width = ''): void { global $PAGE; $renderer = $PAGE->get_renderer('core_question', 'bank'); @@ -115,6 +140,17 @@ abstract class column_base extends view_component { $data['help'] = $help->export_for_template($renderer); } + $data['colname'] = $this->get_column_name(); + $data['columnid'] = $this->get_column_id(); + $data['name'] = $title; + $data['class'] = $name; + $data['width'] = $width; + if (!empty($columnactions)) { + $actions = array_map(fn($columnaction) => $columnaction->get_action_menu_link($this), $columnactions); + $actionmenu = new \action_menu($actions); + $data['actionmenu'] = $actionmenu->export_for_template($renderer); + } + echo $renderer->render_column_header($data); } @@ -216,7 +252,10 @@ abstract class column_base extends view_component { */ protected function display_start($question, $rowclasses): void { $tag = 'td'; - $attr = ['class' => $this->get_classes()]; + $attr = [ + 'class' => $this->get_classes(), + 'data-columnid' => $this->get_column_id(), + ]; if ($this->isheading) { $tag = 'th'; $attr['scope'] = 'row'; @@ -254,6 +293,24 @@ abstract class column_base extends view_component { return (new \ReflectionClass($this))->getShortName(); } + /** + * Return a unique ID for this column object. + * + * This is constructed using the class name and get_column_name(), which must be unique. + * + * The combination of these attributes allows the object to be reconstructed, by splitting the ID into its constituent + * parts then calling {@see from_column_name()}, like this: + * [$class, $columnname] = explode(column_base::ID_SEPARATOR, $columnid, 2); + * $column = $class::from_column_name($qbank, $columnname); + * Including 2 as the $limit parameter for explode() is a good idea for safely, in case a plugin defines a column with the + * ID_SEPARATOR in the column name. + * + * @return string The column ID. + */ + final public function get_column_id(): string { + return implode(self::ID_SEPARATOR, [static::class, $this->get_column_name()]); + } + /** * Any extra class names you would like applied to every cell in this column. * @@ -263,6 +320,15 @@ abstract class column_base extends view_component { return []; } + /** + * Return the default column width in pixels. + * + * @return int + */ + public function get_default_width(): int { + return 120; + } + /** * Output the contents of this column. * @param object $question the row from the $question table, augmented with extra information. @@ -395,4 +461,16 @@ abstract class column_base extends view_component { } } + /** + * Output the column with an example value. + * + * By default, this will call $this->display() using whatever dummy data is passed in. Columns can override this + * to provide example output without requiring valid data. + * + * @param \stdClass $question the row from the $question table, augmented with extra information. + * @param string $rowclasses CSS class names that should be applied to this row of output. + */ + public function display_preview(\stdClass $question, string $rowclasses): void { + $this->display($question, $rowclasses); + } } diff --git a/question/classes/local/bank/column_manager_base.php b/question/classes/local/bank/column_manager_base.php new file mode 100644 index 00000000000..350ada34faf --- /dev/null +++ b/question/classes/local/bank/column_manager_base.php @@ -0,0 +1,73 @@ +. + +namespace core_question\local\bank; + +/** + * Default column manager class + * + * This class defines stub methods that can be overridden by a plugin defining its own column manager. + * + * @package core_question + * @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net} + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class column_manager_base { + /** + * Sort the list of columns + * + * Sort the provided list of columns into the order implemented in this column manager. + * + * @param array $unsortedcolumns Unordered array of columns + * @return array Columns in the desired order. + */ + public function get_sorted_columns(array $unsortedcolumns): array { + return $unsortedcolumns; + } + + /** + * Given an array of columns, set the isvisible attribute. + * + * This base class leave all columns visible. + * + * @param column_base[] $columns + * @return array + */ + public function set_columns_visibility(array $columns): array { + return $columns; + } + + /** + * Return a list of actions to display in an action menu for each column. + * + * @param view $qbank Question bank view. + * @return column_action_base[] A list of column actions. + */ + public function get_column_actions(view $qbank): array { + return []; + } + + /** + * Given a column, return a value for its width CSS property. + * + * @param column_base $column + * @return string CSS width property value. + */ + public function get_column_width(column_base $column): string { + return $column->get_default_width() . 'px'; + } +} diff --git a/question/classes/local/bank/filter_condition_manager.php b/question/classes/local/bank/filter_condition_manager.php index abf1cf2058f..7939feb7c34 100644 --- a/question/classes/local/bank/filter_condition_manager.php +++ b/question/classes/local/bank/filter_condition_manager.php @@ -37,16 +37,23 @@ class filter_condition_manager { * @return array the param and extra param */ public static function extract_parameters_from_fragment_args(array $args): array { - global $DB; - // Decode query string. - $filtercondition = json_decode($args['filtercondition'], true); - $categories = $DB->get_records('question_categories', ['id' => clean_param($filtercondition['cat'], PARAM_INT)]); - $categories = \qbank_managecategories\helper::question_add_context_in_key($categories); - $category = array_pop($categories); - $filtercondition['cat'] = $category->id; - $extraparams = json_decode($args['extraparams'], true); + $params = []; + if (array_key_exists('filter', $args)) { + $params['filter'] = json_decode($args['filter'], true); + } + if (array_key_exists('cmid', $args)) { + $params['cmid'] = $args['cmid']; + } + if (array_key_exists('courseid', $args)) { + $params['courseid'] = $args['courseid']; + } + $params['jointype'] = $args['jointype'] ?? condition::JOINTYPE_DEFAULT; + $params['qpage'] = $args['qpage'] ?? 0; + $params['qperpage'] = $args['qperpage'] ?? 100; + $params['sortdata'] = json_decode($args['sortdata'] ?? '', true); + $extraparams = json_decode($args['extraparams'] ?? '', true); - return [$filtercondition, $extraparams]; + return [$params, $extraparams]; } /** diff --git a/question/classes/local/bank/plugin_features_base.php b/question/classes/local/bank/plugin_features_base.php index 5787da8f6a5..904fa407d51 100644 --- a/question/classes/local/bank/plugin_features_base.php +++ b/question/classes/local/bank/plugin_features_base.php @@ -27,6 +27,11 @@ namespace core_question\local\bank; +use core\context; +use qbank_columnsortorder\local\qbank\column_action_move; +use qbank_columnsortorder\local\qbank\column_action_remove; +use qbank_columnsortorder\local\qbank\column_action_resize; + /** * Class plugin_features_base is the base class for qbank plugins. * @@ -75,6 +80,31 @@ class plugin_features_base { return []; } + /** + * This method will return a column manager object, if this plugin provides one. + * + * @return ?column_manager_base + */ + public function get_column_manager(): ?column_manager_base { + return null; + } + + /** + * This method will return an array of renderable objects, for adding additional controls to the question bank screen. + * + * The array returned can include a numeric index for each object, to indicate the position in which it should be displayed + * relative to other controls. If two plugins return controls with the same position, they will be displayed after one another, + * based on the alphabetical order of the plugin component names. + * + * @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 { + return []; + } + /** * Return search conditions for the plugin. * diff --git a/question/classes/local/bank/view.php b/question/classes/local/bank/view.php index f072c225455..f01b43c0d7e 100644 --- a/question/classes/local/bank/view.php +++ b/question/classes/local/bank/view.php @@ -34,7 +34,7 @@ use core_plugin_manager; use core_question\local\bank\condition; use core_question\local\statistics\statistics_bulk_loader; use core_question\output\question_bank_filter_ui; -use qbank_columnsortorder\column_manager; +use core_question\local\bank\column_manager_base; use qbank_deletequestion\hidden_condition; use qbank_editquestion\editquestion_helper; use qbank_managecategories\category_condition; @@ -193,7 +193,7 @@ class view { protected $pagevars = []; /** - * @var array $plugins all the qbank plugin objects. + * @var plugin_features_base[] $plugins Plugin feature objects for all enabled qbank plugins. */ protected $plugins = []; @@ -212,6 +212,11 @@ class view { */ public $extraparams = []; + /** + * @var column_manager_base $columnmanager The column manager, can be overridden by plugins. + */ + protected $columnmanager; + /** * Constructor for view. * @@ -266,8 +271,9 @@ class view { $this->lastchangedid = clean_param($pageurl->param('lastchanged'), PARAM_INT); + $this->init_plugins(); + $this->init_column_manager(); // Possibly the heading part can be removed. - $this->plugins = \core_component::get_plugin_list_with_class('qbank', 'plugin_feature', 'plugin_feature.php'); $this->set_pagevars($params); $this->init_columns($this->wanted_columns(), $this->heading_column()); $this->init_question_actions(); @@ -276,17 +282,45 @@ class view { } /** - * Initialize bulk actions. + * Get an array of plugin features objects for all enabled qbank plugins. + * + * @return void */ - protected function init_bulk_actions(): void { + protected function init_plugins(): void { $plugins = \core_component::get_plugin_list_with_class('qbank', 'plugin_feature', 'plugin_feature.php'); - foreach ($plugins as $componentname => $plugin) { + foreach ($plugins as $componentname => $pluginclass) { if (!\core\plugininfo\qbank::is_plugin_enabled($componentname)) { continue; } + $this->plugins[$componentname] = new $pluginclass(); + } + // Sort plugin list by component name. + ksort($this->plugins); + } - $pluginentrypoint = new $plugin(); - $bulkactions = $pluginentrypoint->get_bulk_actions(); + /** + * Allow qbank plugins to override the column manager. + * + * If multiple qbank plugins define a column manager, this will pick the first one sorted alphabetically. + * + * @return void + */ + protected function init_column_manager(): void { + $this->columnmanager = new column_manager_base(); + foreach ($this->plugins as $plugin) { + if ($columnmanager = $plugin->get_column_manager()) { + $this->columnmanager = $columnmanager; + break; + } + } + } + + /** + * Initialize bulk actions. + */ + protected function init_bulk_actions(): void { + foreach ($this->plugins as $componentname => $plugin) { + $bulkactions = $plugin->get_bulk_actions(); if (!is_array($bulkactions)) { debugging("The method {$componentname}::get_bulk_actions() must return an " . "array of bulk actions instead of a single bulk action. " . @@ -302,7 +336,6 @@ class view { 'capabilities' => $bulkactionobject->get_bulk_action_capabilities() ]; } - } } @@ -336,14 +369,9 @@ class view { * @return void */ protected function init_question_actions(): void { - $plugins = \core_component::get_plugin_list_with_class('qbank', 'plugin_feature', 'plugin_feature.php'); $this->questionactions = []; - foreach ($plugins as $component => $plugin) { - if (!qbank::is_plugin_enabled($component)) { - continue; - } - $pluginentrypointobject = new $plugin(); - $menuactions = $pluginentrypointobject->get_question_actions($this); + foreach ($this->plugins as $plugin) { + $menuactions = $plugin->get_question_actions($this); foreach ($menuactions as $menuaction) { $this->questionactions[$menuaction::class] = $menuaction; } @@ -355,72 +383,37 @@ class view { * * @return array */ - protected function get_class_for_columns(): array { - $this->corequestionbankcolumns = [ - 'checkbox_column', - 'question_type_column', - 'question_name_idnumber_tags_column', - 'edit_menu_column', - 'export_xml_action_column', - 'question_status_column', - 'version_number_column', - 'creator_name_column', - 'comment_count_column', + protected function get_question_bank_plugins(): array { + $questionbankclasscolumns = []; + $newpluginclasscolumns = []; + $corequestionbankcolumns = [ + 'core_question\local\bank\checkbox_column' . column_base::ID_SEPARATOR . 'checkbox_column', + 'core_question\local\bank\edit_menu_column' . column_base::ID_SEPARATOR . 'edit_menu_column', ]; - if (question_get_display_preference('qbshowtext', 0, PARAM_INT, new \moodle_url(''))) { - $this->corequestionbankcolumns[] = 'question_text_row'; - } - - $questionbankclasscolumns = []; - foreach ($this->corequestionbankcolumns as $fullname) { - $shortname = $fullname; - if (class_exists('core_question\\local\\bank\\' . $fullname)) { - $fullname = 'core_question\\local\\bank\\' . $fullname; - $questionbankclasscolumns[$shortname] = new $fullname($this); - } else { - $questionbankclasscolumns[$shortname] = ''; + foreach ($corequestionbankcolumns as $columnid) { + [$columnclass, $columnname] = explode(column_base::ID_SEPARATOR, $columnid, 2); + if (class_exists($columnclass)) { + $questionbankclasscolumns[$columnid] = $columnclass::from_column_name($this, $columnname); } } - return $questionbankclasscolumns; - } - /** - * Get the list of qbank plugins with available objects for features. - * - * @return array - */ - protected function get_question_bank_plugins(): array { - $newpluginclasscolumns = []; - $questionbankclasscolumns = $this->get_class_for_columns(); - - $plugins = $this->plugins; - foreach ($plugins as $componentname => $plugin) { - $pluginentrypointobject = new $plugin(); - $plugincolumnobjects = $pluginentrypointobject->get_question_columns($this); - // Don't need the plugins without column objects. - if (empty($plugincolumnobjects)) { - unset($plugins[$componentname]); - continue; - } + foreach ($this->plugins as $plugin) { + $plugincolumnobjects = $plugin->get_question_columns($this); foreach ($plugincolumnobjects as $columnobject) { - $columnname = $columnobject->get_column_name(); - foreach ($this->corequestionbankcolumns as $key => $corequestionbankcolumn) { - if (!\core\plugininfo\qbank::is_plugin_enabled($componentname)) { - unset($questionbankclasscolumns[$columnname]); - continue; - } + $columnid = $columnobject->get_column_id(); + foreach ($corequestionbankcolumns as $corequestionbankcolumn) { // Check if it has custom preference selector to view/hide. if ($columnobject->has_preference()) { if (!$columnobject->get_preference()) { continue; } } - if ($corequestionbankcolumn === $columnname) { - $questionbankclasscolumns[$columnname] = $columnobject; + if ($corequestionbankcolumn === $columnid) { + $questionbankclasscolumns[$columnid] = $columnobject; } else { // Any community plugin for column/action. - $newpluginclasscolumns[$columnname] = $columnobject; + $newpluginclasscolumns[$columnid] = $columnobject; } } } @@ -431,15 +424,12 @@ class view { $questionbankclasscolumns[$key] = $newpluginclasscolumn; } - // Check if qbank_columnsortorder is enabled. - if (array_key_exists('columnsortorder', core_plugin_manager::instance()->get_enabled_plugins('qbank'))) { - $columnorder = new column_manager(); - $questionbankclasscolumns = $columnorder->get_sorted_columns($questionbankclasscolumns); - } + $questionbankclasscolumns = $this->columnmanager->get_sorted_columns($questionbankclasscolumns); + $questionbankclasscolumns = $this->columnmanager->set_columns_visibility($questionbankclasscolumns); // Mitigate the error in case of any regression. foreach ($questionbankclasscolumns as $shortname => $questionbankclasscolumn) { - if (!is_object($questionbankclasscolumn)) { + if (!is_object($questionbankclasscolumn) || !$questionbankclasscolumn->isvisible) { unset($questionbankclasscolumns[$shortname]); } } @@ -456,7 +446,7 @@ class view { $this->requiredcolumns = []; $questionbankcolumns = $this->get_question_bank_plugins(); foreach ($questionbankcolumns as $classobject) { - if (empty($classobject)) { + if (empty($classobject) || !($classobject instanceof \core_question\local\bank\column_base)) { continue; } $this->requiredcolumns[$classobject->get_column_name()] = $classobject; @@ -500,7 +490,10 @@ class view { if ($column->is_extra_row()) { $this->extrarows[$column->get_column_name()] = $column; } else { - $this->visiblecolumns[$column->get_column_name()] = $column; + // Only add columns which are visible. + if ($column->isvisible) { + $this->visiblecolumns[$column->get_column_name()] = $column; + } } } @@ -905,7 +898,14 @@ class view { * Shows the question bank interface. */ public function display(): void { - echo \html_writer::start_div('questionbankwindow boxwidthwide boxaligncenter'); + $editcontexts = $this->contexts->having_one_edit_tab_cap('questions'); + + echo \html_writer::start_div('questionbankwindow boxwidthwide boxaligncenter', [ + 'data-component' => 'core_question', + 'data-callback' => 'display_question_bank', + 'data-contextid' => $editcontexts[array_key_last($editcontexts)]->id, + ]); + // Show the filters and search options. $this->wanted_filters(); // Continues with list of questions. @@ -1087,22 +1087,50 @@ class view { } /** - * Create a new question form. + * Does the current view allow adding new questions? * - * @param false|mixed|\stdClass $category - * @param bool $canadd + * @return bool True if the view supports adding new questions. */ - protected function create_new_question_form($category, $canadd): void { - if (\core\plugininfo\qbank::is_plugin_enabled('qbank_editquestion')) { - echo editquestion_helper::create_new_question_button($category->id, - $this->questionactions['qbank_editquestion\edit_action']->editquestionurl->params(), $canadd); + public function allow_add_questions(): bool { + return true; + } + + /** + * Output the question bank controls for each plugin. + * + * Controls will be output in the order defined by the array keys returned from + * {@see plugin_features_base::get_question_bank_controls}. If more than one plugin defines a control in the same position, + * they will placed after one another based on the alphabetical order of the plugins. + * + * @param \core\context $context The current context, for permissions checks. + * @param int $categoryid The current question category. + */ + protected function get_plugin_controls(\core\context $context, int $categoryid): string { + global $OUTPUT; + $orderedcontrols = []; + foreach ($this->plugins as $plugin) { + $plugincontrols = $plugin->get_question_bank_controls($this, $context, $categoryid); + foreach ($plugincontrols as $position => $plugincontrol) { + if (!array_key_exists($position, $orderedcontrols)) { + $orderedcontrols[$position] = []; + } + $orderedcontrols[$position][] = $plugincontrol; + } } + ksort($orderedcontrols); + $output = ''; + foreach ($orderedcontrols as $controls) { + foreach ($controls as $control) { + $output .= $OUTPUT->render($control); + } + } + return $OUTPUT->render_from_template('core_question/question_bank_controls', ['controls' => $output]); } /** * Prints the table of questions in a category with interactions */ - protected function display_question_list(): void { + public function display_question_list(): void { // This function can be moderately slow with large question counts and may time out. // We probably do not want to raise it to unlimited, so randomly picking 5 minutes. // Note: We do not call this in the loop because quiz ob_ captures this function (see raise() PHP doc). @@ -1111,10 +1139,16 @@ class view { [$categoryid, $contextid] = category_condition::validate_category_param($this->pagevars['cat']); $catcontext = \context::instance_by_id($contextid); - $canadd = has_capability('moodle/question:add', $catcontext); - - $category = category_condition::get_category_record($categoryid, $contextid); - $this->create_new_question_form($category, $canadd); + echo \html_writer::start_tag( + 'div', + [ + 'id' => 'questionscontainer', + 'data-component' => $this->component, + 'data-callback' => $this->callback, + 'data-contextid' => $this->get_most_specific_context()->id, + ] + ); + echo $this->get_plugin_controls($catcontext, $categoryid); $this->build_query(); $questionsrs = $this->load_page_questions(); @@ -1134,7 +1168,7 @@ class view { echo \html_writer::input_hidden_params($this->baseurl); echo \html_writer::start_tag('div', - ['class' => 'categoryquestionscontainer', 'id' => 'questionscontainer']); + ['class' => 'categoryquestionscontainer']); if ($totalquestions > 0) { // Bulk load any required statistics. $this->load_required_statistics($questions); @@ -1151,6 +1185,7 @@ class view { echo \html_writer::end_tag('fieldset'); echo \html_writer::end_tag('form'); + echo \html_writer::end_tag('div'); } /** @@ -1370,13 +1405,13 @@ class view { // Start of the table. echo \html_writer::start_tag('table', [ 'id' => 'categoryquestions', - 'class' => 'table-responsive', - 'data-defaultsort' => json_encode($this->sort) + 'class' => 'question-bank-table generaltable', + 'data-defaultsort' => json_encode($this->sort), ]); // Prints the table header. echo \html_writer::start_tag('thead'); - echo \html_writer::start_tag('tr'); + echo \html_writer::start_tag('tr', ['class' => 'qbank-column-list']); $this->print_table_headers(); echo \html_writer::end_tag('tr'); echo \html_writer::end_tag('thead'); @@ -1426,9 +1461,11 @@ class view { /** * Print table headers from child classes. */ - public function print_table_headers(): void { + protected function print_table_headers(): void { + $columnactions = $this->columnmanager->get_column_actions($this); foreach ($this->visiblecolumns as $column) { - $column->display_header(); + $width = $this->columnmanager->get_column_width($column); + $column->display_header($columnactions, $width); } } diff --git a/question/editlib.php b/question/editlib.php index fec2787f91a..78559c9ac53 100644 --- a/question/editlib.php +++ b/question/editlib.php @@ -201,6 +201,9 @@ function question_edit_setup($edittab, $baseurl, $requirecmid = false, $unused = // Category list page. $params['cpage'] = optional_param('cpage', null, PARAM_INT); + // Sort data. + $params['sortdata'] = optional_param_array('sortdata', [], PARAM_INT); + $PAGE->set_pagelayout('admin'); return question_build_edit_resources($edittab, $baseurl, $params); @@ -247,7 +250,7 @@ function question_build_edit_resources($edittab, $baseurl, $params, $thispageurl->remove_all_params(); // We are going to explicity add back everything important - this avoids unwanted params from being retained. $cleanparams = [ - 'qsorts' => [], + 'sortdata' => [], 'filter' => null ]; $paramtypes = [ @@ -258,7 +261,6 @@ function question_build_edit_resources($edittab, $baseurl, $params, 'category' => PARAM_SEQUENCE, 'qperpage' => PARAM_INT, 'cpage' => PARAM_INT, - 'qbshowtext' => PARAM_INT, ]; foreach ($paramtypes as $name => $type) { @@ -276,6 +278,10 @@ function question_build_edit_resources($edittab, $baseurl, $params, $cleanparams['filter'] = $params['filter']; } + if (isset($params['sortdata'])) { + $cleanparams['sortdata'] = clean_param_array($params['sortdata'], PARAM_INT); + } + $cmid = $cleanparams['cmid']; $courseid = $cleanparams['courseid']; $qpage = $cleanparams['qpage'] ?: -1; @@ -283,7 +289,6 @@ function question_build_edit_resources($edittab, $baseurl, $params, $category = $cleanparams['category'] ?: 0; $qperpage = $cleanparams['qperpage']; $cpage = $cleanparams['cpage'] ?: 1; - $qsorts = $cleanparams['qsorts']; if (is_null($cmid) && is_null($courseid)) { throw new \moodle_exception('Must provide a cmid or courseid'); @@ -293,16 +298,21 @@ function question_build_edit_resources($edittab, $baseurl, $params, list($module, $cm) = get_module_from_cmid($cmid); $courseid = $cm->course; $thispageurl->params(compact('cmid')); - require_login($courseid, false, $cm); $thiscontext = context_module::instance($cmid); } else { $module = null; $cm = null; $thispageurl->params(compact('courseid')); - require_login($courseid, false); $thiscontext = context_course::instance($courseid); } + if (defined('AJAX_SCRIPT') && AJAX_SCRIPT) { + // For AJAX, we don't need to set up the course page for output. + require_login(); + } else { + require_login($courseid, false, $cm); + } + if ($thiscontext){ $contexts = new core_question\local\bank\question_edit_contexts($thiscontext); $contexts->require_one_edit_tab_cap($edittab); @@ -330,19 +340,6 @@ function question_build_edit_resources($edittab, $baseurl, $params, navigation_node::override_active_url($thispageurl); } - // This need to occur after the override_active_url call above because - // these values change on the page request causing the URLs to mismatch - // when trying to work out the active node. - for ($i = 1; $i <= core_question\local\bank\view::MAX_SORTS; $i++) { - $param = 'qbs' . $i; - if (isset($params[$param])) { - $value = clean_param($params[$param], PARAM_TEXT); - } else { - break; - } - $thispageurl->param($param, $value); - } - if ($pagevars['qpage'] > -1) { $thispageurl->param('qpage', $pagevars['qpage']); } else { @@ -387,7 +384,7 @@ function question_build_edit_resources($edittab, $baseurl, $params, $pagevars['tabname'] = $edittab; // Sort parameters. - $pagevars['sortdata'] = optional_param_array('sortdata', [], PARAM_INT); + $pagevars['sortdata'] = $cleanparams['sortdata']; foreach ($pagevars['sortdata'] as $sortname => $sortorder) { $thispageurl->param('sortdata[' . $sortname . ']', $sortorder); } diff --git a/question/lib.php b/question/lib.php index b10c10f48ed..60b59a3e6e0 100644 --- a/question/lib.php +++ b/question/lib.php @@ -117,38 +117,35 @@ function core_question_output_fragment_tags_form($args) { * @return array|string */ function core_question_output_fragment_question_data(array $args): string { - global $PAGE; if (empty($args)) { return ''; } [$params, $extraparams] = \core_question\local\bank\filter_condition_manager::extract_parameters_from_fragment_args($args); - $thiscontext = \context_course::instance($params['courseid']); - $contexts = new \core_question\local\bank\question_edit_contexts($thiscontext); - $contexts->require_one_edit_tab_cap($params['tabname']); - $course = get_course($params['courseid']); + [ + $thispageurl, + $contexts, + , + $cm, + , + $pagevars + ] = question_build_edit_resources('questions', '/question/edit.php', $params); + + if (is_null($cm)) { + $course = get_course(clean_param($args['courseid'], PARAM_INT)); + } else { + $course = get_course($cm->course); + } $viewclass = empty($args['view']) ? \core_question\local\bank\view::class : clean_param($args['view'], PARAM_NOTAGS); - $cmid = clean_param($args['cmid'], PARAM_INT); - [, $cm] = empty($cmid) ? [null, null] : get_module_from_cmid($cmid); - $nodeparent = $PAGE->settingsnav->find('questionbank', \navigation_node::TYPE_CONTAINER); - $thispageurl = new \moodle_url($nodeparent->action); - if ($cm) { - $thispageurl->param('cmid', $cm->id); - } else { - $thispageurl->param('courseid', $params['courseid']); - } - if (!empty($args['filterquery'])) { - $thispageurl->param('filter', $args['filterquery']); - } if (!empty($args['lastchanged'])) { - $thispageurl->param('lastchanged', $args['lastchanged']); + $thispageurl->param('lastchanged', clean_param($args['lastchanged'], PARAM_INT)); } - if (!empty($params['sortdata'])) { - foreach ($params['sortdata'] as $sortname => $sortorder) { - $thispageurl->param('sortdata[' . $sortname . ']', $sortorder); - } - } - $questionbank = new $viewclass($contexts, $thispageurl, $course, $cm, $params, $extraparams); - return $questionbank->display_questions_table(); + // This is highly suspicious, but it is the same approach taken in /question/edit.php. See MDL-79281. + $thispageurl->param('deleteall', 1); + $questionbank = new $viewclass($contexts, $thispageurl, $course, $cm, $pagevars, $extraparams); + $questionbank->add_standard_search_conditions(); + ob_start(); + $questionbank->display_question_list(); + return ob_get_clean(); } diff --git a/question/templates/column_header.mustache b/question/templates/column_header.mustache index 660e437102a..50558ca47a3 100644 --- a/question/templates/column_header.mustache +++ b/question/templates/column_header.mustache @@ -43,31 +43,42 @@ Example context (json): { "sortable": false, - "extraclasses": "checkbox pr-3", - "tiptitle": "\n