diff --git a/lang/en/user.php b/lang/en/user.php index e010e0948e1..0c768b1c8cd 100644 --- a/lang/en/user.php +++ b/lang/en/user.php @@ -22,7 +22,12 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +$string['addcondition'] = 'Add condition'; +$string['applyfilters'] = 'Apply filters'; +$string['clearfilterrow'] = 'Remove filter row'; +$string['clearfilters'] = 'Clear filters'; $string['countparticipantsfound'] = '{$a} participants found'; +$string['match'] = 'Match'; $string['privacy:courserequestpath'] = 'Requested courses'; $string['privacy:descriptionpath'] = 'Profile description'; $string['privacy:devicespath'] = 'User devices'; @@ -126,6 +131,8 @@ $string['privacy:passwordresetpath'] = 'Password resets'; $string['privacy:profileimagespath'] = 'Profile images'; $string['privacy:privatefilespath'] = 'Private files'; $string['privacy:sessionpath'] = 'Session data'; +$string['selectfiltertype'] = 'Select'; $string['target:upcomingactivitiesdue'] = 'Upcoming activities due'; $string['target:upcomingactivitiesdue_help'] = 'This target generates reminders for upcoming activities due.'; $string['target:upcomingactivitiesdueinfo'] = 'All upcoming activities due insights are listed here. These students have received these insights directly.'; +$string['typeorselect'] = 'Type or select...'; diff --git a/user/amd/build/local/participantsfilter/filter.min.js b/user/amd/build/local/participantsfilter/filter.min.js new file mode 100644 index 00000000000..cb8df333ee8 --- /dev/null +++ b/user/amd/build/local/participantsfilter/filter.min.js @@ -0,0 +1,2 @@ +define ("core_user/local/participantsfilter/filter",["exports","core/form-autocomplete","./selectors","core/str"],function(a,b,c,d){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;b=e(b);c=e(c);function e(a){return a&&a.__esModule?a:{default:a}}function f(a,b,c,d,e,f,g){try{var h=a[f](g),i=h.value}catch(a){c(a);return}if(h.done){b(i)}else{Promise.resolve(i).then(d,e)}}function g(a){return function(){var b=this,c=arguments;return new Promise(function(d,e){var i=a.apply(b,c);function g(a){f(i,d,e,g,h,"next",a)}function h(a){f(i,d,e,g,h,"throw",a)}g(void 0)})}}function h(a,b){if(!(a instanceof b)){throw new TypeError("Cannot call a class as a function")}}function i(a,b){for(var c=0,d;c.\n\n/**\n * Base Filter class for a filter type in the participants filter UI.\n *\n * @module core_user/local/participantsfilter/filter\n * @package core_user\n * @copyright 2020 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport Autocomplete from 'core/form-autocomplete';\nimport Selectors from './selectors';\nimport {get_string as getString} from 'core/str';\n\n/**\n * Fetch all checked options in the select.\n *\n * This is a poor-man's polyfill for select.selectedOptions, which is not available in IE11.\n *\n * @param {HTMLSelectElement} select\n * @returns {HTMLOptionElement[]} All selected options\n */\nconst getOptionsForSelect = select => {\n return select.querySelectorAll(':checked');\n};\n\nexport default class {\n\n /**\n * Constructor for a new filter.\n *\n * @param {String} filterType The type of filter that this relates to\n * @param {HTMLElement} rootNode The root node for the participants filterset\n */\n constructor(filterType, rootNode) {\n this.filterType = filterType;\n this.rootNode = rootNode;\n\n this.addValueSelector();\n }\n\n /**\n * Perform any tear-down for this filter type.\n */\n tearDown() {\n // eslint-disable-line no-empty-function\n }\n\n /**\n * Add the value selector to the filter row.\n */\n async addValueSelector() {\n const filterValueNode = this.getFilterValueNode();\n\n // Copy the data in place.\n filterValueNode.innerHTML = this.getSourceDataForFilter().outerHTML;\n\n const dataSource = filterValueNode.querySelector('select');\n\n Autocomplete.enhance(\n // The source select element.\n dataSource,\n\n // Whether to allow 'tags' (custom entries).\n dataSource.dataset.allowCustom == \"1\",\n\n // We do not require AJAX at all as standard.\n null,\n\n // The string to use as a placeholder.\n await getString('typeorselect', 'core_user'),\n\n // Disable case sensitivity on searches.\n false,\n\n // Show suggestions.\n true,\n\n // Do not override the 'no suggestions' string.\n null,\n\n // Close the suggestions if this is not a multi-select.\n !dataSource.multiple\n );\n }\n\n /**\n * Get the root node for this filter.\n *\n * @returns {HTMLElement}\n */\n get filterRoot() {\n return this.rootNode.querySelector(Selectors.filter.byName(this.filterType));\n }\n\n /**\n * Get the possible data for this filter type.\n *\n * @returns {Array}\n */\n getSourceDataForFilter() {\n const filterDataNode = this.rootNode.querySelector(Selectors.filterset.regions.datasource);\n\n return filterDataNode.querySelector(Selectors.data.fields.byName(this.filterType));\n }\n\n /**\n * Get the HTMLElement which contains the value selector.\n *\n * @returns {HTMLElement}\n */\n getFilterValueNode() {\n return this.filterRoot.querySelector(Selectors.filter.regions.values);\n }\n\n /**\n * Get the name of this filter.\n *\n * @returns {String}\n */\n get name() {\n return this.filterType;\n }\n\n /**\n * Get the type of join specified.\n *\n * @returns {Number}\n */\n get jointype() {\n return this.filterRoot.querySelector(Selectors.filter.fields.join).value;\n }\n\n /**\n * Get the list of raw values for this filter type.\n *\n * @returns {Array}\n */\n get rawValues() {\n const filterValueNode = this.getFilterValueNode();\n const filterValueSelect = filterValueNode.querySelector('select');\n\n return Object.values(getOptionsForSelect(filterValueSelect)).map(option => option.value);\n }\n\n /**\n * Get the list of values for this filter type.\n *\n * @returns {Array}\n */\n get values() {\n return this.rawValues.map(option => parseInt(option, 10));\n }\n\n /**\n * Get the composed value for this filter.\n *\n * @returns {Object}\n */\n get filterValue() {\n return {\n name: this.name,\n jointype: this.jointype,\n values: this.values,\n };\n }\n}\n"],"file":"filter.min.js"} \ No newline at end of file diff --git a/user/amd/build/local/participantsfilter/filtertypes/courseid.min.js b/user/amd/build/local/participantsfilter/filtertypes/courseid.min.js new file mode 100644 index 00000000000..61445dc5cfd --- /dev/null +++ b/user/amd/build/local/participantsfilter/filtertypes/courseid.min.js @@ -0,0 +1,2 @@ +define ("core_user/local/participantsfilter/filtertypes/courseid",["exports","../filter"],function(a,b){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;b=function(a){return a&&a.__esModule?a:{default:a}}(b);function c(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){c=function(a){return typeof a}}else{c=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return c(a)}function d(a,b,c,d,e,f,g){try{var h=a[f](g),i=h.value}catch(a){c(a);return}if(h.done){b(i)}else{Promise.resolve(i).then(d,e)}}function e(a){return function(){var b=this,c=arguments;return new Promise(function(e,f){var i=a.apply(b,c);function g(a){d(i,e,f,g,h,"next",a)}function h(a){d(i,e,f,g,h,"throw",a)}g(void 0)})}}function f(a,b){if(!(a instanceof b)){throw new TypeError("Cannot call a class as a function")}}function g(a,b){for(var c=0,d;c.\n\n/**\n * Course ID filter.\n *\n * @module core_user/local/participantsfilter/filtertypes/courseid\n * @package core_user\n * @copyright 2020 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport Filter from '../filter';\n\nexport default class extends Filter {\n constructor(filterType, filterSet) {\n super(filterType, filterSet);\n }\n\n async addValueSelector() {\n // eslint-disable-line no-empty-function\n }\n\n /**\n * Get the composed value for this filter.\n *\n * @returns {Object}\n */\n get filterValue() {\n return {\n name: this.name,\n jointype: 1,\n values: [parseInt(this.rootNode.dataset.tableCourseId, 10)],\n };\n }\n}\n"],"file":"courseid.min.js"} \ No newline at end of file diff --git a/user/amd/build/local/participantsfilter/selectors.min.js b/user/amd/build/local/participantsfilter/selectors.min.js new file mode 100644 index 00000000000..1f1dcc5dc02 --- /dev/null +++ b/user/amd/build/local/participantsfilter/selectors.min.js @@ -0,0 +1,2 @@ +define ("core_user/local/participantsfilter/selectors",["exports"],function(a){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;var b=function(a){return"[data-filterregion=\"".concat(a,"\"]")},c=function(a){return"[data-filteraction=\"".concat(a,"\"]")},d=function(a){return"[data-filterfield=\"".concat(a,"\"]")},e={filter:{region:b("filter"),actions:{remove:c("remove")},fields:{join:d("join"),type:d("type")},regions:{values:b("value")},byName:function byName(a){return"".concat(b("filter"),"[data-filter-type=\"").concat(a,"\"]")}},filterset:{region:b("actions"),actions:{addRow:c("add"),applyFilters:c("apply"),resetFilters:c("reset")},regions:{filterlist:b("filters"),datasource:b("filtertypedata")}},data:{fields:{byName:function byName(a){return"[data-field-name=\"".concat(a,"\"]")}},typeList:b("filtertypelist")}};a.default=e;return a.default}); +//# sourceMappingURL=selectors.min.js.map diff --git a/user/amd/build/local/participantsfilter/selectors.min.js.map b/user/amd/build/local/participantsfilter/selectors.min.js.map new file mode 100644 index 00000000000..31cd5e66e02 --- /dev/null +++ b/user/amd/build/local/participantsfilter/selectors.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../../src/local/participantsfilter/selectors.js"],"names":["getFilterRegion","region","getFilterAction","action","getFilterField","field","filter","actions","remove","fields","join","type","regions","values","byName","name","filterset","addRow","applyFilters","resetFilters","filterlist","datasource","data","typeList"],"mappings":"iKAwBMA,CAAAA,CAAe,CAAG,SAAAC,CAAM,uCAA2BA,CAA3B,Q,CACxBC,CAAe,CAAG,SAAAC,CAAM,uCAA2BA,CAA3B,Q,CACxBC,CAAc,CAAG,SAAAC,CAAK,sCAA0BA,CAA1B,Q,GAEb,CACXC,MAAM,CAAE,CACJL,MAAM,CAAED,CAAe,CAAC,QAAD,CADnB,CAEJO,OAAO,CAAE,CACLC,MAAM,CAAEN,CAAe,CAAC,QAAD,CADlB,CAFL,CAKJO,MAAM,CAAE,CACJC,IAAI,CAAEN,CAAc,CAAC,MAAD,CADhB,CAEJO,IAAI,CAAEP,CAAc,CAAC,MAAD,CAFhB,CALJ,CASJQ,OAAO,CAAE,CACLC,MAAM,CAAEb,CAAe,CAAC,OAAD,CADlB,CATL,CAYJc,MAAM,CAAE,gBAAAC,CAAI,kBAAOf,CAAe,CAAC,QAAD,CAAtB,gCAAsDe,CAAtD,QAZR,CADG,CAeXC,SAAS,CAAE,CACPf,MAAM,CAAED,CAAe,CAAC,SAAD,CADhB,CAEPO,OAAO,CAAE,CACLU,MAAM,CAAEf,CAAe,CAAC,KAAD,CADlB,CAELgB,YAAY,CAAEhB,CAAe,CAAC,OAAD,CAFxB,CAGLiB,YAAY,CAAEjB,CAAe,CAAC,OAAD,CAHxB,CAFF,CAOPU,OAAO,CAAE,CACLQ,UAAU,CAAEpB,CAAe,CAAC,SAAD,CADtB,CAELqB,UAAU,CAAErB,CAAe,CAAC,gBAAD,CAFtB,CAPF,CAfA,CA2BXsB,IAAI,CAAE,CACFb,MAAM,CAAE,CACJK,MAAM,CAAE,gBAAAC,CAAI,qCAAyBA,CAAzB,QADR,CADN,CAIFQ,QAAQ,CAAEvB,CAAe,CAAC,gBAAD,CAJvB,CA3BK,C","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 * Module containing the selectors for user filters.\n *\n * @module core_user/local/user_filter/selectors\n * @package core_user\n * @copyright 2020 Michael Hawkins \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nconst getFilterRegion = region => `[data-filterregion=\"${region}\"]`;\nconst getFilterAction = action => `[data-filteraction=\"${action}\"]`;\nconst getFilterField = field => `[data-filterfield=\"${field}\"]`;\n\nexport default {\n filter: {\n region: getFilterRegion('filter'),\n actions: {\n remove: getFilterAction('remove'),\n },\n fields: {\n join: getFilterField('join'),\n type: getFilterField('type'),\n },\n regions: {\n values: getFilterRegion('value'),\n },\n byName: name => `${getFilterRegion('filter')}[data-filter-type=\"${name}\"]`,\n },\n filterset: {\n region: getFilterRegion('actions'),\n actions: {\n addRow: getFilterAction('add'),\n applyFilters: getFilterAction('apply'),\n resetFilters: getFilterAction('reset'),\n },\n regions: {\n filterlist: getFilterRegion('filters'),\n datasource: getFilterRegion('filtertypedata'),\n },\n },\n data: {\n fields: {\n byName: name => `[data-field-name=\"${name}\"]`,\n },\n typeList: getFilterRegion('filtertypelist'),\n },\n};\n"],"file":"selectors.min.js"} \ No newline at end of file diff --git a/user/amd/build/participantsfilter.min.js b/user/amd/build/participantsfilter.min.js new file mode 100644 index 00000000000..31b031a3683 --- /dev/null +++ b/user/amd/build/participantsfilter.min.js @@ -0,0 +1,2 @@ +function _typeof(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){_typeof=function(a){return typeof a}}else{_typeof=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return _typeof(a)}define ("core_user/participantsfilter",["exports","./local/participantsfilter/filtertypes/courseid","core_table/dynamic","./local/participantsfilter/filter","core/notification","./local/participantsfilter/selectors","core/templates"],function(a,b,c,d,e,f,g){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=void 0;b=j(b);c=i(c);d=j(d);e=j(e);f=j(f);g=j(g);var m="undefined"!=typeof window?window:"undefined"!=typeof self?self:"undefined"!=typeof global?global:{};function h(){if("function"!=typeof WeakMap)return null;var a=new WeakMap;h=function(){return a};return a}function i(a){if(a&&a.__esModule){return a}if(null===a||"object"!==_typeof(a)&&"function"!=typeof a){return{default:a}}var b=h();if(b&&b.has(a)){return b.get(a)}var c={},d=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var e in a){if(Object.prototype.hasOwnProperty.call(a,e)){var f=d?Object.getOwnPropertyDescriptor(a,e):null;if(f&&(f.get||f.set)){Object.defineProperty(c,e,f)}else{c[e]=a[e]}}}c.default=a;if(b){b.set(a,c)}return c}function j(a){return a&&a.__esModule?a:{default:a}}function k(a,b,c,d,e,f,g){try{var h=a[f](g),i=h.value}catch(a){c(a);return}if(h.done){b(i)}else{Promise.resolve(i).then(d,e)}}function l(a){return function(){var b=this,c=arguments;return new Promise(function(d,e){var h=a.apply(b,c);function f(a){k(h,d,e,f,g,"next",a)}function g(a){k(h,d,e,f,g,"throw",a)}f(void 0)})}}var n=function(a){var h=document.querySelector("#".concat(a)),i={courseid:new b.default("courseid",h)},j=function(){return h.querySelector(f.default.filterset.regions.filterlist)},k=function(){return g.default.renderForPromise("core_user/local/participantsfilter/filterrow",{}).then(function(a){var b=a.html,c=a.js,d=g.default.appendNodeContents(j(),b,c);return d}).then(function(a){var b=h.querySelector(f.default.data.typeList);a.forEach(function(a){var c=a.querySelector(f.default.filter.fields.type);if(c){c.innerHTML=b.innerHTML}});return a}).then(function(a){v();return a}).catch(e.default.exception)},n=function(a){var b=h.querySelector(f.default.filterset.regions.datasource);return b.querySelector(f.default.data.fields.byName(a))},o=function(){var a=l(regeneratorRuntime.mark(function a(b,c){var e,g,j;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:b.dataset.filterType=c;e=n(c);g=d.default;if(!e.dataset.filterTypeClass){a.next=7;break}a.next=6;return"function"==typeof m.define&&m.define.amd?new Promise(function(a,b){m.require([e.dataset.filterTypeClass],a,b)}):"undefined"!=typeof module&&module.exports&&"undefined"!=typeof require||"undefined"!=typeof module&&module.component&&m.require&&"component"===m.require.loader?Promise.resolve(require((e.dataset.filterTypeClass))):Promise.resolve(m[e.dataset.filterTypeClass]);case 6:g=a.sent;case 7:i[c]=new g(c,h);j=b.querySelector(f.default.filter.fields.type);j.disabled="disabled";v();case 11:case"end":return a.stop();}}},a)}));return function(){return a.apply(this,arguments)}}(),p=function(a){return i[a]},q=function(a){var b=j().querySelectorAll(f.default.filter.region).length;if(1===b){s(a)}else{r(a)}},r=function(a){t(a.dataset.filterType);a.remove();w();v()},s=function(a){t(a.dataset.filterType);return g.default.renderForPromise("core_user/local/participantsfilter/filterrow",{}).then(function(b){var c=b.html,d=b.js,e=g.default.replaceNode(a,c,d);return e}).then(function(a){var b=h.querySelector(f.default.data.typeList);a.forEach(function(a){var c=a.querySelector(f.default.filter.fields.type);if(c){c.innerHTML=b.innerHTML}});return a}).then(function(a){v();return a}).then(function(a){w();return a}).catch(e.default.exception)},t=function(a){if(a){var b=p(a);if(b){b.tearDown();delete i[a]}}},u=function(){var a=l(regeneratorRuntime.mark(function a(){var b;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:b=j().querySelectorAll(f.default.filter.region);b.forEach(function(a){q(a)});w();case 3:case"end":return a.stop();}}},a)}));return function(){return a.apply(this,arguments)}}(),v=function(){var a=j().querySelectorAll(f.default.filter.region);a.forEach(function(a){var b=a.querySelectorAll(f.default.filter.fields.type+" option");b.forEach(function(b){if(b.value===a.dataset.filterType){b.classList.remove("hidden");b.disabled=!1}else if(i[b.value]){b.classList.add("hidden");b.disabled=!0}else{b.classList.remove("hidden");b.disabled=!1}})})},w=function(){return c.setFilters(c.getTableFromId(h.dataset.tableRegion),{filters:Object.values(i).map(function(a){return a.filterValue}),jointype:1})};h.querySelector(f.default.filterset.region).addEventListener("click",function(a){if(a.target.closest(f.default.filterset.actions.addRow)){a.preventDefault();k()}if(a.target.closest(f.default.filterset.actions.applyFilters)){a.preventDefault();w()}if(a.target.closest(f.default.filterset.actions.resetFilters)){a.preventDefault();u()}});h.querySelector(f.default.filterset.regions.filterlist).addEventListener("click",function(a){if(a.target.closest(f.default.filter.actions.remove)){a.preventDefault();q(a.target.closest(f.default.filter.region))}});h.querySelector(f.default.filterset.regions.filterlist).addEventListener("change",function(a){var b=a.target.closest(f.default.filter.fields.type);if(b&&b.value){var c=a.target.closest(f.default.filter.region);o(c,b.value)}})};a.init=n}); +//# sourceMappingURL=participantsfilter.min.js.map diff --git a/user/amd/build/participantsfilter.min.js.map b/user/amd/build/participantsfilter.min.js.map new file mode 100644 index 00000000000..1a05bb3bc55 --- /dev/null +++ b/user/amd/build/participantsfilter.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../src/participantsfilter.js"],"names":["init","participantsRegionId","filterSet","document","querySelector","activeFilters","courseid","CourseFilter","getFilterRegion","Selectors","filterset","regions","filterlist","addFilterRow","Templates","renderForPromise","then","html","js","newContentNodes","appendNodeContents","filterRow","typeList","data","forEach","contentNode","contentTypeList","filter","fields","type","innerHTML","updateFiltersOptions","catch","Notification","exception","getFilterDataSource","filterType","filterDataNode","datasource","byName","addFilter","dataset","Filter","GenericFilter","filterTypeClass","typeField","disabled","getFilterObject","name","removeOrReplaceFilterRow","filterCount","querySelectorAll","region","length","replaceFilterRow","removeFilterRow","removeFilterObject","remove","updateTableFromFilter","replaceNode","filterName","tearDown","removeAllFilters","filters","options","option","value","classList","add","DynamicTable","setFilters","getTableFromId","tableRegion","Object","values","map","filterValue","jointype","addEventListener","e","target","closest","actions","addRow","preventDefault","applyFilters","resetFilters"],"mappings":"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","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 * Participants filter managemnet.\n *\n * @module core_user/participants_filter\n * @package core_user\n * @copyright 2020 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport CourseFilter from './local/participantsfilter/filtertypes/courseid';\nimport * as DynamicTable from 'core_table/dynamic';\nimport GenericFilter from './local/participantsfilter/filter';\nimport Notification from 'core/notification';\nimport Selectors from './local/participantsfilter/selectors';\nimport Templates from 'core/templates';\n\n/**\n * Initialise the participants filter on the element with the given id.\n *\n * @param {String} participantsRegionId\n */\nexport const init = participantsRegionId => {\n // Keep a reference to the filterset.\n const filterSet = document.querySelector(`#${participantsRegionId}`);\n\n // Keep a reference to all of the active filters.\n const activeFilters = {\n courseid: new CourseFilter('courseid', filterSet),\n };\n\n /**\n * Get the filter list region.\n *\n * @return {HTMLElement}\n */\n const getFilterRegion = () => filterSet.querySelector(Selectors.filterset.regions.filterlist);\n\n /**\n * Add an unselected filter row.\n *\n * @return {Promise}\n */\n const addFilterRow = () => {\n return Templates.renderForPromise('core_user/local/participantsfilter/filterrow', {})\n .then(({html, js}) => {\n const newContentNodes = Templates.appendNodeContents(getFilterRegion(), html, js);\n\n return newContentNodes;\n })\n .then(filterRow => {\n // Note: This is a nasty hack.\n // We should try to find a better way of doing this.\n // We do not have the list of types in a readily consumable format, so we take the pre-rendered one and copy\n // it in place.\n const typeList = filterSet.querySelector(Selectors.data.typeList);\n\n filterRow.forEach(contentNode => {\n const contentTypeList = contentNode.querySelector(Selectors.filter.fields.type);\n\n if (contentTypeList) {\n contentTypeList.innerHTML = typeList.innerHTML;\n }\n });\n\n return filterRow;\n })\n .then(filterRow => {\n updateFiltersOptions();\n\n return filterRow;\n })\n .catch(Notification.exception);\n };\n\n /**\n * Get the filter data source node fro the specified filter type.\n *\n * @param {String} filterType\n * @return {HTMLElement}\n */\n const getFilterDataSource = filterType => {\n const filterDataNode = filterSet.querySelector(Selectors.filterset.regions.datasource);\n\n return filterDataNode.querySelector(Selectors.data.fields.byName(filterType));\n };\n\n /**\n * Add a filter to the list of active filters, performing any necessary setup.\n *\n * @param {HTMLElement} filterRow\n * @param {String} filterType\n */\n const addFilter = async(filterRow, filterType) => {\n // Name the filter on the filter row.\n filterRow.dataset.filterType = filterType;\n\n const filterDataNode = getFilterDataSource(filterType);\n\n // Instantiate the Filter class.\n let Filter = GenericFilter;\n if (filterDataNode.dataset.filterTypeClass) {\n Filter = await import(filterDataNode.dataset.filterTypeClass);\n }\n activeFilters[filterType] = new Filter(filterType, filterSet);\n\n // Disable the select.\n const typeField = filterRow.querySelector(Selectors.filter.fields.type);\n typeField.disabled = 'disabled';\n\n // Update the list of available filter types.\n updateFiltersOptions();\n };\n\n /**\n * Get the registered filter class for the named filter.\n *\n * @param {String} name\n * @return {Object} See the Filter class.\n */\n const getFilterObject = name => {\n return activeFilters[name];\n };\n\n /**\n * Remove or replace the specified filter row and associated class, ensuring that if there is only one filter row,\n * that it is replaced instead of being removed.\n *\n * @param {HTMLElement} filterRow\n */\n const removeOrReplaceFilterRow = filterRow => {\n const filterCount = getFilterRegion().querySelectorAll(Selectors.filter.region).length;\n\n if (filterCount === 1) {\n replaceFilterRow(filterRow);\n } else {\n removeFilterRow(filterRow);\n }\n };\n\n /**\n * Remove the specified filter row and associated class.\n *\n * @param {HTMLElement} filterRow\n */\n const removeFilterRow = filterRow => {\n // Remove the filter object.\n removeFilterObject(filterRow.dataset.filterType);\n\n // Remove the actual filter HTML.\n filterRow.remove();\n\n // Refresh the table.\n updateTableFromFilter();\n\n // Update the list of available filter types.\n updateFiltersOptions();\n };\n\n /**\n * Replace the specified filter row with a new one.\n *\n * @param {HTMLElement} filterRow\n * @return {Promise}\n */\n const replaceFilterRow = filterRow => {\n // Remove the filter object.\n removeFilterObject(filterRow.dataset.filterType);\n\n return Templates.renderForPromise('core_user/local/participantsfilter/filterrow', {})\n .then(({html, js}) => {\n const newContentNodes = Templates.replaceNode(filterRow, html, js);\n\n return newContentNodes;\n })\n .then(filterRow => {\n // Note: This is a nasty hack.\n // We should try to find a better way of doing this.\n // We do not have the list of types in a readily consumable format, so we take the pre-rendered one and copy\n // it in place.\n const typeList = filterSet.querySelector(Selectors.data.typeList);\n\n filterRow.forEach(contentNode => {\n const contentTypeList = contentNode.querySelector(Selectors.filter.fields.type);\n\n if (contentTypeList) {\n contentTypeList.innerHTML = typeList.innerHTML;\n }\n });\n\n return filterRow;\n })\n .then(filterRow => {\n updateFiltersOptions();\n\n return filterRow;\n })\n .then(filterRow => {\n // Refresh the table.\n updateTableFromFilter();\n\n return filterRow;\n })\n .catch(Notification.exception);\n };\n\n /**\n * Remove the Filter Object from the register.\n *\n * @param {string} filterName The name of the filter to be removed\n */\n const removeFilterObject = filterName => {\n if (filterName) {\n const filter = getFilterObject(filterName);\n if (filter) {\n filter.tearDown();\n\n // Remove from the list of active filters.\n delete activeFilters[filterName];\n }\n }\n };\n\n /**\n * Remove all filters.\n */\n const removeAllFilters = async() => {\n const filters = getFilterRegion().querySelectorAll(Selectors.filter.region);\n filters.forEach((filterRow) => {\n removeOrReplaceFilterRow(filterRow);\n });\n\n // Refresh the table.\n updateTableFromFilter();\n };\n\n /**\n * Update the list of filter types to filter out those already selected.\n */\n const updateFiltersOptions = () => {\n const filters = getFilterRegion().querySelectorAll(Selectors.filter.region);\n filters.forEach(filterRow => {\n const options = filterRow.querySelectorAll(Selectors.filter.fields.type + ' option');\n options.forEach(option => {\n if (option.value === filterRow.dataset.filterType) {\n option.classList.remove('hidden');\n option.disabled = false;\n } else if (activeFilters[option.value]) {\n option.classList.add('hidden');\n option.disabled = true;\n } else {\n option.classList.remove('hidden');\n option.disabled = false;\n }\n });\n });\n };\n\n /**\n * Update the Dynamic table based upon the current filter.\n *\n * @return {Promise}\n */\n const updateTableFromFilter = () => {\n // TODO The main join type does not exist yet.\n const joinType = 1;\n\n return DynamicTable.setFilters(\n DynamicTable.getTableFromId(filterSet.dataset.tableRegion),\n {\n filters: Object.values(activeFilters).map(filter => filter.filterValue),\n jointype: joinType,\n }\n );\n };\n\n // Add listeners for the main actions.\n filterSet.querySelector(Selectors.filterset.region).addEventListener('click', e => {\n if (e.target.closest(Selectors.filterset.actions.addRow)) {\n e.preventDefault();\n\n addFilterRow();\n }\n\n if (e.target.closest(Selectors.filterset.actions.applyFilters)) {\n e.preventDefault();\n\n updateTableFromFilter();\n }\n\n if (e.target.closest(Selectors.filterset.actions.resetFilters)) {\n e.preventDefault();\n\n removeAllFilters();\n }\n });\n\n // Add the listener to remove a single filter.\n filterSet.querySelector(Selectors.filterset.regions.filterlist).addEventListener('click', e => {\n if (e.target.closest(Selectors.filter.actions.remove)) {\n e.preventDefault();\n\n removeOrReplaceFilterRow(e.target.closest(Selectors.filter.region));\n }\n });\n\n // Add listeners for the filter type selection.\n filterSet.querySelector(Selectors.filterset.regions.filterlist).addEventListener('change', e => {\n const typeField = e.target.closest(Selectors.filter.fields.type);\n if (typeField && typeField.value) {\n const filter = e.target.closest(Selectors.filter.region);\n\n addFilter(filter, typeField.value);\n }\n });\n};\n"],"file":"participantsfilter.min.js"} \ No newline at end of file diff --git a/user/amd/src/local/participantsfilter/filter.js b/user/amd/src/local/participantsfilter/filter.js new file mode 100644 index 00000000000..2cc72afec34 --- /dev/null +++ b/user/amd/src/local/participantsfilter/filter.js @@ -0,0 +1,180 @@ +// 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 . + +/** + * Base Filter class for a filter type in the participants filter UI. + * + * @module core_user/local/participantsfilter/filter + * @package core_user + * @copyright 2020 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +import Autocomplete from 'core/form-autocomplete'; +import Selectors from './selectors'; +import {get_string as getString} from 'core/str'; + +/** + * Fetch all checked options in the select. + * + * This is a poor-man's polyfill for select.selectedOptions, which is not available in IE11. + * + * @param {HTMLSelectElement} select + * @returns {HTMLOptionElement[]} All selected options + */ +const getOptionsForSelect = select => { + return select.querySelectorAll(':checked'); +}; + +export default class { + + /** + * Constructor for a new filter. + * + * @param {String} filterType The type of filter that this relates to + * @param {HTMLElement} rootNode The root node for the participants filterset + */ + constructor(filterType, rootNode) { + this.filterType = filterType; + this.rootNode = rootNode; + + this.addValueSelector(); + } + + /** + * Perform any tear-down for this filter type. + */ + tearDown() { + // eslint-disable-line no-empty-function + } + + /** + * Add the value selector to the filter row. + */ + async addValueSelector() { + const filterValueNode = this.getFilterValueNode(); + + // Copy the data in place. + filterValueNode.innerHTML = this.getSourceDataForFilter().outerHTML; + + const dataSource = filterValueNode.querySelector('select'); + + Autocomplete.enhance( + // The source select element. + dataSource, + + // Whether to allow 'tags' (custom entries). + dataSource.dataset.allowCustom == "1", + + // We do not require AJAX at all as standard. + null, + + // The string to use as a placeholder. + await getString('typeorselect', 'core_user'), + + // Disable case sensitivity on searches. + false, + + // Show suggestions. + true, + + // Do not override the 'no suggestions' string. + null, + + // Close the suggestions if this is not a multi-select. + !dataSource.multiple + ); + } + + /** + * Get the root node for this filter. + * + * @returns {HTMLElement} + */ + get filterRoot() { + return this.rootNode.querySelector(Selectors.filter.byName(this.filterType)); + } + + /** + * Get the possible data for this filter type. + * + * @returns {Array} + */ + getSourceDataForFilter() { + const filterDataNode = this.rootNode.querySelector(Selectors.filterset.regions.datasource); + + return filterDataNode.querySelector(Selectors.data.fields.byName(this.filterType)); + } + + /** + * Get the HTMLElement which contains the value selector. + * + * @returns {HTMLElement} + */ + getFilterValueNode() { + return this.filterRoot.querySelector(Selectors.filter.regions.values); + } + + /** + * Get the name of this filter. + * + * @returns {String} + */ + get name() { + return this.filterType; + } + + /** + * Get the type of join specified. + * + * @returns {Number} + */ + get jointype() { + return this.filterRoot.querySelector(Selectors.filter.fields.join).value; + } + + /** + * Get the list of raw values for this filter type. + * + * @returns {Array} + */ + get rawValues() { + const filterValueNode = this.getFilterValueNode(); + const filterValueSelect = filterValueNode.querySelector('select'); + + return Object.values(getOptionsForSelect(filterValueSelect)).map(option => option.value); + } + + /** + * Get the list of values for this filter type. + * + * @returns {Array} + */ + get values() { + return this.rawValues.map(option => parseInt(option, 10)); + } + + /** + * Get the composed value for this filter. + * + * @returns {Object} + */ + get filterValue() { + return { + name: this.name, + jointype: this.jointype, + values: this.values, + }; + } +} diff --git a/user/amd/src/local/participantsfilter/filtertypes/courseid.js b/user/amd/src/local/participantsfilter/filtertypes/courseid.js new file mode 100644 index 00000000000..49698846219 --- /dev/null +++ b/user/amd/src/local/participantsfilter/filtertypes/courseid.js @@ -0,0 +1,47 @@ +// 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 . + +/** + * Course ID filter. + * + * @module core_user/local/participantsfilter/filtertypes/courseid + * @package core_user + * @copyright 2020 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +import Filter from '../filter'; + +export default class extends Filter { + constructor(filterType, filterSet) { + super(filterType, filterSet); + } + + async addValueSelector() { + // eslint-disable-line no-empty-function + } + + /** + * Get the composed value for this filter. + * + * @returns {Object} + */ + get filterValue() { + return { + name: this.name, + jointype: 1, + values: [parseInt(this.rootNode.dataset.tableCourseId, 10)], + }; + } +} diff --git a/user/amd/src/local/participantsfilter/selectors.js b/user/amd/src/local/participantsfilter/selectors.js new file mode 100644 index 00000000000..d17b28e28f3 --- /dev/null +++ b/user/amd/src/local/participantsfilter/selectors.js @@ -0,0 +1,62 @@ +// 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 . + +/** + * Module containing the selectors for user filters. + * + * @module core_user/local/user_filter/selectors + * @package core_user + * @copyright 2020 Michael Hawkins + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +const getFilterRegion = region => `[data-filterregion="${region}"]`; +const getFilterAction = action => `[data-filteraction="${action}"]`; +const getFilterField = field => `[data-filterfield="${field}"]`; + +export default { + filter: { + region: getFilterRegion('filter'), + actions: { + remove: getFilterAction('remove'), + }, + fields: { + join: getFilterField('join'), + type: getFilterField('type'), + }, + regions: { + values: getFilterRegion('value'), + }, + byName: name => `${getFilterRegion('filter')}[data-filter-type="${name}"]`, + }, + filterset: { + region: getFilterRegion('actions'), + actions: { + addRow: getFilterAction('add'), + applyFilters: getFilterAction('apply'), + resetFilters: getFilterAction('reset'), + }, + regions: { + filterlist: getFilterRegion('filters'), + datasource: getFilterRegion('filtertypedata'), + }, + }, + data: { + fields: { + byName: name => `[data-field-name="${name}"]`, + }, + typeList: getFilterRegion('filtertypelist'), + }, +}; diff --git a/user/amd/src/participantsfilter.js b/user/amd/src/participantsfilter.js new file mode 100644 index 00000000000..23e54327e06 --- /dev/null +++ b/user/amd/src/participantsfilter.js @@ -0,0 +1,330 @@ +// 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 . + +/** + * Participants filter managemnet. + * + * @module core_user/participants_filter + * @package core_user + * @copyright 2020 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import CourseFilter from './local/participantsfilter/filtertypes/courseid'; +import * as DynamicTable from 'core_table/dynamic'; +import GenericFilter from './local/participantsfilter/filter'; +import Notification from 'core/notification'; +import Selectors from './local/participantsfilter/selectors'; +import Templates from 'core/templates'; + +/** + * Initialise the participants filter on the element with the given id. + * + * @param {String} participantsRegionId + */ +export const init = participantsRegionId => { + // Keep a reference to the filterset. + const filterSet = document.querySelector(`#${participantsRegionId}`); + + // Keep a reference to all of the active filters. + const activeFilters = { + courseid: new CourseFilter('courseid', filterSet), + }; + + /** + * Get the filter list region. + * + * @return {HTMLElement} + */ + const getFilterRegion = () => filterSet.querySelector(Selectors.filterset.regions.filterlist); + + /** + * Add an unselected filter row. + * + * @return {Promise} + */ + const addFilterRow = () => { + return Templates.renderForPromise('core_user/local/participantsfilter/filterrow', {}) + .then(({html, js}) => { + const newContentNodes = Templates.appendNodeContents(getFilterRegion(), html, js); + + return newContentNodes; + }) + .then(filterRow => { + // Note: This is a nasty hack. + // We should try to find a better way of doing this. + // We do not have the list of types in a readily consumable format, so we take the pre-rendered one and copy + // it in place. + const typeList = filterSet.querySelector(Selectors.data.typeList); + + filterRow.forEach(contentNode => { + const contentTypeList = contentNode.querySelector(Selectors.filter.fields.type); + + if (contentTypeList) { + contentTypeList.innerHTML = typeList.innerHTML; + } + }); + + return filterRow; + }) + .then(filterRow => { + updateFiltersOptions(); + + return filterRow; + }) + .catch(Notification.exception); + }; + + /** + * Get the filter data source node fro the specified filter type. + * + * @param {String} filterType + * @return {HTMLElement} + */ + const getFilterDataSource = filterType => { + const filterDataNode = filterSet.querySelector(Selectors.filterset.regions.datasource); + + return filterDataNode.querySelector(Selectors.data.fields.byName(filterType)); + }; + + /** + * Add a filter to the list of active filters, performing any necessary setup. + * + * @param {HTMLElement} filterRow + * @param {String} filterType + */ + const addFilter = async(filterRow, filterType) => { + // Name the filter on the filter row. + filterRow.dataset.filterType = filterType; + + const filterDataNode = getFilterDataSource(filterType); + + // Instantiate the Filter class. + let Filter = GenericFilter; + if (filterDataNode.dataset.filterTypeClass) { + Filter = await import(filterDataNode.dataset.filterTypeClass); + } + activeFilters[filterType] = new Filter(filterType, filterSet); + + // Disable the select. + const typeField = filterRow.querySelector(Selectors.filter.fields.type); + typeField.disabled = 'disabled'; + + // Update the list of available filter types. + updateFiltersOptions(); + }; + + /** + * Get the registered filter class for the named filter. + * + * @param {String} name + * @return {Object} See the Filter class. + */ + const getFilterObject = name => { + return activeFilters[name]; + }; + + /** + * Remove or replace the specified filter row and associated class, ensuring that if there is only one filter row, + * that it is replaced instead of being removed. + * + * @param {HTMLElement} filterRow + */ + const removeOrReplaceFilterRow = filterRow => { + const filterCount = getFilterRegion().querySelectorAll(Selectors.filter.region).length; + + if (filterCount === 1) { + replaceFilterRow(filterRow); + } else { + removeFilterRow(filterRow); + } + }; + + /** + * Remove the specified filter row and associated class. + * + * @param {HTMLElement} filterRow + */ + const removeFilterRow = filterRow => { + // Remove the filter object. + removeFilterObject(filterRow.dataset.filterType); + + // Remove the actual filter HTML. + filterRow.remove(); + + // Refresh the table. + updateTableFromFilter(); + + // Update the list of available filter types. + updateFiltersOptions(); + }; + + /** + * Replace the specified filter row with a new one. + * + * @param {HTMLElement} filterRow + * @return {Promise} + */ + const replaceFilterRow = filterRow => { + // Remove the filter object. + removeFilterObject(filterRow.dataset.filterType); + + return Templates.renderForPromise('core_user/local/participantsfilter/filterrow', {}) + .then(({html, js}) => { + const newContentNodes = Templates.replaceNode(filterRow, html, js); + + return newContentNodes; + }) + .then(filterRow => { + // Note: This is a nasty hack. + // We should try to find a better way of doing this. + // We do not have the list of types in a readily consumable format, so we take the pre-rendered one and copy + // it in place. + const typeList = filterSet.querySelector(Selectors.data.typeList); + + filterRow.forEach(contentNode => { + const contentTypeList = contentNode.querySelector(Selectors.filter.fields.type); + + if (contentTypeList) { + contentTypeList.innerHTML = typeList.innerHTML; + } + }); + + return filterRow; + }) + .then(filterRow => { + updateFiltersOptions(); + + return filterRow; + }) + .then(filterRow => { + // Refresh the table. + updateTableFromFilter(); + + return filterRow; + }) + .catch(Notification.exception); + }; + + /** + * Remove the Filter Object from the register. + * + * @param {string} filterName The name of the filter to be removed + */ + const removeFilterObject = filterName => { + if (filterName) { + const filter = getFilterObject(filterName); + if (filter) { + filter.tearDown(); + + // Remove from the list of active filters. + delete activeFilters[filterName]; + } + } + }; + + /** + * Remove all filters. + */ + const removeAllFilters = async() => { + const filters = getFilterRegion().querySelectorAll(Selectors.filter.region); + filters.forEach((filterRow) => { + removeOrReplaceFilterRow(filterRow); + }); + + // Refresh the table. + updateTableFromFilter(); + }; + + /** + * Update the list of filter types to filter out those already selected. + */ + const updateFiltersOptions = () => { + const filters = getFilterRegion().querySelectorAll(Selectors.filter.region); + filters.forEach(filterRow => { + const options = filterRow.querySelectorAll(Selectors.filter.fields.type + ' option'); + options.forEach(option => { + if (option.value === filterRow.dataset.filterType) { + option.classList.remove('hidden'); + option.disabled = false; + } else if (activeFilters[option.value]) { + option.classList.add('hidden'); + option.disabled = true; + } else { + option.classList.remove('hidden'); + option.disabled = false; + } + }); + }); + }; + + /** + * Update the Dynamic table based upon the current filter. + * + * @return {Promise} + */ + const updateTableFromFilter = () => { + // TODO The main join type does not exist yet. + const joinType = 1; + + return DynamicTable.setFilters( + DynamicTable.getTableFromId(filterSet.dataset.tableRegion), + { + filters: Object.values(activeFilters).map(filter => filter.filterValue), + jointype: joinType, + } + ); + }; + + // Add listeners for the main actions. + filterSet.querySelector(Selectors.filterset.region).addEventListener('click', e => { + if (e.target.closest(Selectors.filterset.actions.addRow)) { + e.preventDefault(); + + addFilterRow(); + } + + if (e.target.closest(Selectors.filterset.actions.applyFilters)) { + e.preventDefault(); + + updateTableFromFilter(); + } + + if (e.target.closest(Selectors.filterset.actions.resetFilters)) { + e.preventDefault(); + + removeAllFilters(); + } + }); + + // Add the listener to remove a single filter. + filterSet.querySelector(Selectors.filterset.regions.filterlist).addEventListener('click', e => { + if (e.target.closest(Selectors.filter.actions.remove)) { + e.preventDefault(); + + removeOrReplaceFilterRow(e.target.closest(Selectors.filter.region)); + } + }); + + // Add listeners for the filter type selection. + filterSet.querySelector(Selectors.filterset.regions.filterlist).addEventListener('change', e => { + const typeField = e.target.closest(Selectors.filter.fields.type); + if (typeField && typeField.value) { + const filter = e.target.closest(Selectors.filter.region); + + addFilter(filter, typeField.value); + } + }); +}; diff --git a/user/classes/output/participants_filter.php b/user/classes/output/participants_filter.php new file mode 100644 index 00000000000..12dddd63dcd --- /dev/null +++ b/user/classes/output/participants_filter.php @@ -0,0 +1,150 @@ +. + +/** + * Class for rendering user filters on the course participants page. + * + * @package core_user + * @copyright 2020 Michael Hawkins + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_user\output; + +use context_course; +use renderable; +use renderer_base; +use stdClass; +use templatable; + +/** + * Class for rendering user filters on the course participants page. + * + * @copyright 2020 Michael Hawkins + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class participants_filter implements renderable, templatable { + + /** @var context_course $context The context where the filters are being rendered. */ + protected $context; + + /** @var string $tableregionid The table to be updated by this filter */ + protected $tableregionid; + + /** + * Participants filter constructor. + * + * @param context_course $context The context where the filters are being rendered. + * @param string $tableregionid The table to be updated by this filter + */ + public function __construct(context_course $context, string $tableregionid) { + $this->context = $context; + $this->tableregionid = $tableregionid; + } + + /** + * Get data for all filter types. + * + * @return array + */ + protected function get_filtertypes(): array { + $filtertypes = []; + + if ($filtertype = $this->get_enrolmentstatus_filter()) { + $filtertypes[] = $filtertype; + } + + return $filtertypes; + } + + /** + * Get data for the enrolment status filter. + * + * @return stdClass|null + */ + protected function get_enrolmentstatus_filter(): ?stdClass { + if (!has_capability('moodle/course:enrolreview', $this->context)) { + return null; + } + + return $this->get_filter_object( + 'status', + get_string('participationstatus', 'core_enrol'), + false, + true, + null, + [ + (object) [ + 'value' => ENROL_USER_ACTIVE, + 'title' => get_string('active'), + ], + (object) [ + 'value' => ENROL_USER_SUSPENDED, + 'title' => get_string('inactive'), + ], + ] + ); + } + + /** + * Export the renderer data in a mustache template friendly format. + * + * @param renderer_base $output Unused. + * @return stdClass Data in a format compatible with a mustache template. + */ + public function export_for_template(renderer_base $output): stdClass { + return (object) [ + 'tableregionid' => $this->tableregionid, + 'courseid' => $this->context->instanceid, + 'filtertypes' => $this->get_filtertypes(), + ]; + + return $data; + } + + /** + * Get a standardised filter object. + * + * @param string $name + * @param string $title + * @param bool $custom + * @param bool $multiple + * @param string|null $filterclass + * @param array $values + * @return stdClass|null + */ + protected function get_filter_object( + string $name, + string $title, + bool $custom, + bool $multiple, + ?string $filterclass, + array $values + ): ?stdClass { + if (empty($values)) { + // Do not show empty filters. + return null; + } + + return (object) [ + 'name' => $name, + 'title' => $title, + 'allowcustom' => $custom, + 'allowmultiple' => $multiple, + 'filtertypeclass' => $filterclass, + 'values' => $values, + ]; + } +} diff --git a/user/index.php b/user/index.php index 3078724573d..cf0f155c878 100644 --- a/user/index.php +++ b/user/index.php @@ -143,6 +143,8 @@ $lastaccess = 0; $searchkeywords = []; $enrolid = 0; +$participanttable = new \core_user\table\participants("user-index-participants-{$course->id}"); + $filterset = new \core_user\table\participants_filterset(); $filterset->add_filter(new integer_filter('courseid', filter::JOINTYPE_DEFAULT, [(int)$course->id])); $enrolfilter = new integer_filter('enrolments'); @@ -249,6 +251,10 @@ echo html_writer::div($enrolbuttonsout, 'float-right', [ $renderer = $PAGE->get_renderer('core_user'); echo $renderer->unified_filter($course, $context, $filtersapplied, $baseurl); +// Render the user filters. +$userrenderer = $PAGE->get_renderer('core_user'); +echo $userrenderer->participants_filter($context, $participanttable->uniqueid); + echo '
'; // Add filters to the baseurl after creating unified_filter to avoid losing them. diff --git a/user/renderer.php b/user/renderer.php index 11f2190e5c8..7be0ce5103d 100644 --- a/user/renderer.php +++ b/user/renderer.php @@ -259,6 +259,20 @@ class core_user_renderer extends plugin_renderer_base { return $this->output->render_from_template('core_user/unified_filter', $context); } + /** + * Render the data required for the participants filter on the course participants page. + * + * @param context $context The context of the course being displayed + * @param string $tableregionid The table to be updated by this filter + * @return string + */ + public function participants_filter(context $context, string $tableregionid): string { + $renderable = new \core_user\output\participants_filter($context, $tableregionid); + $templatecontext = $renderable->export_for_template($this->output); + + return $this->output->render_from_template('core_user/participantsfilter', $templatecontext); + } + /** * Returns a formatted filter option. * diff --git a/user/templates/local/participantsfilter/filterrow.mustache b/user/templates/local/participantsfilter/filterrow.mustache new file mode 100644 index 00000000000..c97bab55d3b --- /dev/null +++ b/user/templates/local/participantsfilter/filterrow.mustache @@ -0,0 +1,56 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_user/local/participantsfilter/filterrow + + Template for use by each filter condition. + + Context variables required for this template: + * filtertypes - Array of filter types available. + + Example context (json): + { + "filtertypes": [ + { + "name": "status", + "title": "Status" + } + ] + } +}} +
+ + + + + + +
+ + +
diff --git a/user/templates/local/participantsfilter/filtertype.mustache b/user/templates/local/participantsfilter/filtertype.mustache new file mode 100644 index 00000000000..f38eb931786 --- /dev/null +++ b/user/templates/local/participantsfilter/filtertype.mustache @@ -0,0 +1,61 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_user/local/participantsfilter/filtertype + + Filter type data, not shown to users but used as a source of data for form autocompletion. + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * filtertypes + + Example context (json): + { + "name": "status", + "title": "Enrolment Status", + "allowcustom": "0", + "allowmultiple" false, + "filtertypeclass": "core_user/local/participantsfilter/filtertypes/courseid", + "values": [ + { + "value": "0", + "title": "Inactive" + }, + { + "value": "1", + "title": "Active" + } + ] + } +}} + diff --git a/user/templates/local/participantsfilter/filtertypes.mustache b/user/templates/local/participantsfilter/filtertypes.mustache new file mode 100644 index 00000000000..b8aea7c8789 --- /dev/null +++ b/user/templates/local/participantsfilter/filtertypes.mustache @@ -0,0 +1,64 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_user/local/participantsfilter/filtertypes + + Placeholder to fetch all filter types. + + Classes required for JS: + * none + + Data attributes required for JS: + * data-filterregion="filtertypedata" + + Context variables required for this template: + * filtertypes + + Example context (json): + { + "filtertypes": [ + { + "name": "status", + "title": "Enrolment Status", + "allowcustom": "0", + "values": [ + { + "value": "0", + "title": "Inactive" + }, + { + "value": "1", + "title": "Active" + } + ] + } + ] + } +}} + + diff --git a/user/templates/participantsfilter.mustache b/user/templates/participantsfilter.mustache new file mode 100644 index 00000000000..15a26ba034f --- /dev/null +++ b/user/templates/participantsfilter.mustache @@ -0,0 +1,67 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_user/participantsfilter + + Template for the form containing one or more filter rows. + + Example context (json): + { + "filtertypes": [ + { + "name": "status", + "title": "Status", + "values": [ + { + "value": 1, + "title": "Active" + }, + { + "value": 0, + "title": "Suspended" + } + ] + } + ] + } +}} + +
+
+ {{> core_user/local/participantsfilter/filterrow }} +
+ +
+   + + +
+ + +
+
+ + {{> core_user/local/participantsfilter/filtertypes}} +
+ +{{#js}} +require(['core_user/participantsfilter'], function(ParticipantsFilter) { + ParticipantsFilter.init('core_user-participantsfilter-{{uniqid}}'); +}); +{{/js}}