diff --git a/lib/templates/datafilter/filter.mustache b/lib/templates/datafilter/filter.mustache index 55b4b0e3faf..8acf1afae4c 100644 --- a/lib/templates/datafilter/filter.mustache +++ b/lib/templates/datafilter/filter.mustache @@ -36,7 +36,8 @@ } ] } - ] + ], + "showallbutton": false } }} @@ -52,7 +53,10 @@ - + {{#showallbutton}} + + {{/showallbutton}} + diff --git a/mod/quiz/classes/question/bank/custom_view.php b/mod/quiz/classes/question/bank/custom_view.php index c396c784207..2387ef67cd5 100644 --- a/mod/quiz/classes/question/bank/custom_view.php +++ b/mod/quiz/classes/question/bank/custom_view.php @@ -83,10 +83,10 @@ class custom_view extends \core_question\local\bank\view { } $this->init_columns($this->wanted_columns(), $this->heading_column()); + $this->pagesize = self::DEFAULT_PAGE_SIZE; parent::__construct($contexts, $pageurl, $course, $cm, $params, $extraparams); [$this->quiz, ] = get_module_from_cmid($cm->id); $this->set_quiz_has_attempts(quiz_has_attempts($this->quiz->id)); - $this->pagesize = self::DEFAULT_PAGE_SIZE; } /** diff --git a/question/amd/build/filter.min.js b/question/amd/build/filter.min.js index 0b4c7adac54..ae290e54059 100644 --- a/question/amd/build/filter.min.js +++ b/question/amd/build/filter.min.js @@ -1,10 +1,10 @@ -define("core_question/filter",["exports","core/datafilter","core/notification","core/datafilter/selectors","core/templates","core/fragment"],(function(_exports,_datafilter,_notification,_selectors,_templates,_fragment){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} +define("core_question/filter",["exports","core/datafilter","core/notification","core/datafilter/selectors","core/templates","core/fragment","core/str","core/loadingicon"],(function(_exports,_datafilter,_notification,_selectors,_templates,_fragment,_str,_loadingicon){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * Question bank filter management. * * @module core_question/filter * @copyright 2021 Tomo Tsuyuki * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_datafilter=_interopRequireDefault(_datafilter),_notification=_interopRequireDefault(_notification),_selectors=_interopRequireDefault(_selectors),_templates=_interopRequireDefault(_templates),_fragment=_interopRequireDefault(_fragment);_exports.init=(filterRegionId,defaultcourseid,defaultcategoryid,perpage,contextId,component,callback,view,cmid,pagevars,extraparams)=>{var _document$querySelect,_document$querySelect2,_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:JSON.stringify(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.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=async(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]",SELECTORS_SHOW_ALL_LINK='[data-filteraction="showall"]',filterSet=document.querySelector("#".concat(filterRegionId)),[showAllText,showPerPageText]=await Promise.all([(0,_str.getString)("showall","core",""),(0,_str.getString)("showperpage","core",extraparams.defaultqperpage)]),viewData={extraparams:JSON.stringify(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);const questionscontainer=document.querySelector(SELECTORS_QUESTION_CONTAINER_ID);questionscontainer.innerHTML="",(0,_loadingicon.addIconToContainerRemoveOnCompletion)(questionscontainer,pendingPromise),_fragment.default.loadFragment(component,callback,contextId,viewData).then(((questionhtml,jsfooter)=>(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.querySelector(".questionbankwindow").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),showallLink=e.target.closest(SELECTORS_SHOW_ALL_LINK);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)}})(),showallLink&&(e.preventDefault(),0===Number(showallLink.dataset.status)?(viewData.qperpage=extraparams.maxqperpage,showallLink.dataset.status=1,showallLink.innerText=showPerPageText):(viewData.qperpage=extraparams.defaultqperpage,showallLink.dataset.status=0,showallLink.innerText=showAllText),viewData.qpage=0,coreFilter.updateTableFromFilter())}));let jointype=null;if(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 2e80c7d9db5..db505ed0713 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 {Object} pagevars JSON-encoded parameters from passed from the view, including filters and jointype.\n * @param {Object} 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: JSON.stringify(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 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","JSON","stringify","cat","courseid","filter","jointype","qpage","qperpage","sortdata","lastchanged","_document$querySelect2","value","sortData","defaultSort","_document$querySelect3","dataset","_document$querySelect4","defaultsort","parse","coreFilter","CoreFilter","filterdata","pendingPromise","parseInt","filterverb","Object","keys","length","isNaN","updateUrlParams","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":"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"} \ 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';\nimport {getString} from 'core/str';\nimport {addIconToContainerRemoveOnCompletion} from 'core/loadingicon';\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 {Object} pagevars JSON-encoded parameters from passed from the view, including filters and jointype.\n * @param {Object} extraparams JSON-encoded additional parameters specific to this view class, used for re-rendering the view.\n */\nexport const init = async(\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 SHOW_ALL_LINK: '[data-filteraction=\"showall\"]',\n };\n\n const filterSet = document.querySelector(`#${filterRegionId}`);\n\n const [\n showAllText,\n showPerPageText,\n ] = await Promise.all([\n getString('showall', 'core', ''),\n getString('showperpage', 'core', extraparams.defaultqperpage),\n ]);\n\n const viewData = {\n extraparams: JSON.stringify(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\n const questionscontainer = document.querySelector(SELECTORS.QUESTION_CONTAINER_ID);\n // Clear the contents of the element, then append the loading icon.\n questionscontainer.innerHTML = '';\n addIconToContainerRemoveOnCompletion(questionscontainer, pendingPromise);\n\n Fragment.loadFragment(component, callback, contextId, viewData)\n // Render questions for first page and pagination.\n .then((questionhtml, jsfooter) => {\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.querySelector('.questionbankwindow').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 const showallLink = e.target.closest(SELECTORS.SHOW_ALL_LINK);\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 if (showallLink) {\n\n e.preventDefault();\n\n // Toggle between showing all and going back to the original qperpage.\n if (Number(showallLink.dataset.status) === 0) {\n viewData.qperpage = extraparams.maxqperpage;\n showallLink.dataset.status = 1;\n showallLink.innerText = showPerPageText;\n } else {\n viewData.qperpage = extraparams.defaultqperpage;\n showallLink.dataset.status = 0;\n showallLink.innerText = showAllText;\n }\n viewData.qpage = 0;\n coreFilter.updateTableFromFilter();\n }\n });\n\n // Run apply filter at page load.\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":["async","filterRegionId","defaultcourseid","defaultcategoryid","perpage","contextId","component","callback","view","cmid","pagevars","extraparams","SELECTORS","filterSet","document","querySelector","showAllText","showPerPageText","Promise","all","defaultqperpage","viewData","JSON","stringify","cat","courseid","filter","jointype","qpage","qperpage","sortdata","lastchanged","_document$querySelect2","value","sortData","defaultSort","_document$querySelect3","dataset","_document$querySelect4","defaultsort","parse","coreFilter","CoreFilter","filterdata","pendingPromise","parseInt","filterverb","Object","keys","length","isNaN","updateUrlParams","questionscontainer","innerHTML","loadFragment","then","questionhtml","jsfooter","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","showallLink","preventDefault","oldSort","sortname","sortorder","updateTableFromFilter","paginationURL","getAttribute","get","search","queryString","urlParams","URLSearchParams","has","cleanedUrl","replace","cleanUrlParams","Number","status","maxqperpage","innerText","entries","emptyFilterRow","regions","remove","rowcount","urlFilter","filtertype","values","filteroptions","rownum","addFilterRow","join","fields","querySelectorAll","forEach","option","disabled"],"mappings":"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"} \ No newline at end of file diff --git a/question/amd/src/filter.js b/question/amd/src/filter.js index b25e880f13a..204ee36fb58 100644 --- a/question/amd/src/filter.js +++ b/question/amd/src/filter.js @@ -26,6 +26,8 @@ import Notification from 'core/notification'; import Selectors from 'core/datafilter/selectors'; import Templates from 'core/templates'; import Fragment from 'core/fragment'; +import {getString} from 'core/str'; +import {addIconToContainerRemoveOnCompletion} from 'core/loadingicon'; /** * Initialise the question bank filter on the element with the given id. @@ -42,7 +44,7 @@ import Fragment from 'core/fragment'; * @param {Object} pagevars JSON-encoded parameters from passed from the view, including filters and jointype. * @param {Object} extraparams JSON-encoded additional parameters specific to this view class, used for re-rendering the view. */ -export const init = ( +export const init = async( filterRegionId, defaultcourseid, defaultcategoryid, @@ -66,10 +68,19 @@ export const init = ( MENU_ACTIONS: '.menu-action', EDIT_SWITCH: '.editmode-switch-form input[name=setmode]', EDIT_SWITCH_URL: '.editmode-switch-form input[name=pageurl]', + SHOW_ALL_LINK: '[data-filteraction="showall"]', }; const filterSet = document.querySelector(`#${filterRegionId}`); + const [ + showAllText, + showPerPageText, + ] = await Promise.all([ + getString('showall', 'core', ''), + getString('showperpage', 'core', extraparams.defaultqperpage), + ]); + const viewData = { extraparams: JSON.stringify(extraparams), cmid, @@ -115,10 +126,15 @@ export const init = ( // Load questions for first page. viewData.filter = JSON.stringify(filterdata); viewData.sortdata = JSON.stringify(sortData); + + const questionscontainer = document.querySelector(SELECTORS.QUESTION_CONTAINER_ID); + // Clear the contents of the element, then append the loading icon. + questionscontainer.innerHTML = ''; + addIconToContainerRemoveOnCompletion(questionscontainer, pendingPromise); + Fragment.loadFragment(component, callback, contextId, viewData) // Render questions for first page and pagination. .then((questionhtml, jsfooter) => { - const questionscontainer = document.querySelector(SELECTORS.QUESTION_CONTAINER_ID); if (questionhtml === undefined) { questionhtml = ''; } @@ -180,10 +196,11 @@ export const init = ( }; // Add listeners for the sorting, paging and clear actions. - document.addEventListener('click', e => { + document.querySelector('.questionbankwindow').addEventListener('click', e => { const sortableLink = e.target.closest(SELECTORS.SORT_LINK); const paginationLink = e.target.closest(SELECTORS.PAGINATION_LINK); const clearLink = e.target.closest(Selectors.filterset.actions.resetFilters); + const showallLink = e.target.closest(SELECTORS.SHOW_ALL_LINK); if (sortableLink) { e.preventDefault(); const oldSort = sortData; @@ -209,6 +226,23 @@ export const init = ( if (clearLink) { cleanUrlParams(); } + if (showallLink) { + + e.preventDefault(); + + // Toggle between showing all and going back to the original qperpage. + if (Number(showallLink.dataset.status) === 0) { + viewData.qperpage = extraparams.maxqperpage; + showallLink.dataset.status = 1; + showallLink.innerText = showPerPageText; + } else { + viewData.qperpage = extraparams.defaultqperpage; + showallLink.dataset.status = 0; + showallLink.innerText = showAllText; + } + viewData.qpage = 0; + coreFilter.updateTableFromFilter(); + } }); // Run apply filter at page load. diff --git a/question/classes/local/bank/view.php b/question/classes/local/bank/view.php index ff5f50851f8..988061f4ecd 100644 --- a/question/classes/local/bank/view.php +++ b/question/classes/local/bank/view.php @@ -235,6 +235,10 @@ class view { $this->cm = $cm; $this->extraparams = $extraparams; + // Add the default qperpage to extra params array so we can switch back and forth between it and "all". + $this->extraparams['defaultqperpage'] = $this->pagesize; + $this->extraparams['maxqperpage'] = MAXIMUM_QUESTIONS_PER_PAGE; + // Default filter condition. if (!isset($params['filter']) && isset($params['cat'])) { $params['filter'] = []; @@ -811,15 +815,18 @@ class view { */ protected function load_page_questions(): \moodle_recordset { global $DB; + + // Load the questions based on the page we are on. $questions = $DB->get_recordset_sql($this->loadsql, $this->sqlparams, - (int)$this->pagevars['qpage'] * (int)$this->pagevars['qperpage'], $this->pagevars['qperpage']); + (int)$this->pagevars['qpage'] * (int)$this->pagevars['qperpage'], (int)$this->pagevars['qperpage']); + if (!$questions->valid()) { $questions->close(); // No questions on this page. Reset to the nearest page that contains questions. $this->pagevars['qpage'] = max(0, - ceil($this->totalcount / $this->pagevars['qperpage']) - 1); + ceil($this->totalcount / (int)$this->pagevars['qperpage']) - 1); $questions = $DB->get_recordset_sql($this->loadsql, $this->sqlparams, - $this->pagevars['qpage'] * (int) $this->pagevars['qperpage'], $this->pagevars['qperpage']); + (int)$this->pagevars['qpage'] * (int)$this->pagevars['qperpage'], (int)$this->pagevars['qperpage']); } return $questions; } @@ -1155,6 +1162,7 @@ class view { // We probably do not want to raise it to unlimited, so randomly picking 5 minutes. // Note: We do not call this in the loop because quiz ob_ captures this function (see raise() PHP doc). \core_php_time_limit::raise(300); + raise_memory_limit(MEMORY_EXTRA); [$categoryid, $contextid] = category_condition::validate_category_param($this->pagevars['cat']); $catcontext = \context::instance_by_id($contextid); diff --git a/question/classes/output/question_bank_filter_ui.php b/question/classes/output/question_bank_filter_ui.php index 5f48a823107..c31598801fd 100644 --- a/question/classes/output/question_bank_filter_ui.php +++ b/question/classes/output/question_bank_filter_ui.php @@ -137,6 +137,7 @@ class question_bank_filter_ui extends datafilter { 'cmid' => $this->cmid ?? 0, 'pagevars' => json_encode($this->pagevars), 'extraparams' => json_encode($this->extraparams), + 'showallbutton' => true, ]; } } diff --git a/question/tests/behat/filter_questions_show_all.feature b/question/tests/behat/filter_questions_show_all.feature new file mode 100644 index 00000000000..2a98877a110 --- /dev/null +++ b/question/tests/behat/filter_questions_show_all.feature @@ -0,0 +1,75 @@ +@core @core_question @qbank_filter @javascript +Feature: A teacher can show all of the questions on the question bank and override pagination + In order to see all the questions in the question bank + As a teacher + I must be able to toggle between show all and show paginated + + 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 | topics | + And the following "activities" exist: + | activity | name | course | idnumber | + | quiz | Test quiz | C1 | quiz1 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + And the following "question categories" exist: + | contextlevel | reference | questioncategory | name | + | Course | C1 | Top | Test Category | + Given 201 "questions" exist with the following data: + | questioncategory | Test Category | + | qtype | truefalse | + | name | Question #[count] | + | questiontext | True or false? | + + Scenario: Use "Show All" button on the question bank when there are no questions + Given I am on the "Course 1" "core_question > course question bank" page logged in as "teacher1" + When I apply question bank filter "Category" with value "Default for C1" + Then I should not see "Question #" + And I click on "Show all" "button" + Then I should not see "Question #" + And I click on "Show 100 per page" "button" + Then I should not see "Question #" + + Scenario: Question bank shows paginated questions by default + Given I am on the "Course 1" "core_question > course question bank" page logged in as "teacher1" + When I apply question bank filter "Category" with value "Top for Course 1" + Then I should see "100" occurrences of "Question #" in the "div#questionscontainer" "css_element" + And I should see "1" in the ".pagination" "css_element" + And I should see "2" in the ".pagination" "css_element" + And I should see "3" in the ".pagination" "css_element" + + Scenario: Toggle "Show all" shows all questions + Given I am on the "Course 1" "core_question > course question bank" page logged in as "teacher1" + When I apply question bank filter "Category" with value "Top for Course 1" + And I click on "Show all" "button" + Then I should see "201" occurrences of "Question #" in the "div#questionscontainer" "css_element" + And ".pagination" "css_element" should not exist + And I should see "Show 100 per page" + + Scenario: Toggle "Show all" back to paginated shows paginated questions + Given I am on the "Course 1" "core_question > course question bank" page logged in as "teacher1" + When I apply question bank filter "Category" with value "Top for Course 1" + And I click on "Show all" "button" + And I click on "Show 100 per page" "button" + Then I should see "100" occurrences of "Question #" in the "div#questionscontainer" "css_element" + And I should see "1" in the ".pagination" "css_element" + And I should see "2" in the ".pagination" "css_element" + And I should see "3" in the ".pagination" "css_element" + And I should see "Show all" + + Scenario: Show all questions on the question bank when adding questions to a quiz + Given I am on the "Test quiz" "mod_quiz > Edit" page logged in as "teacher1" + When I open the "last" add to quiz menu + And I follow "from question bank" + And I apply question bank filter "Category" with value "Top for Course 1" + Then I should see "20" occurrences of "Question #" in the "div#questionscontainer" "css_element" + When I click on "Show all" "button" + Then I should see "201" occurrences of "Question #" in the "div#questionscontainer" "css_element" + When I click on "Select all" "checkbox" + And I click on "Add selected questions to the quiz" "button" + Then I should see "Questions: 201"