diff --git a/lang/en/reportbuilder.php b/lang/en/reportbuilder.php index 8cdcde75a83..eae1e7ac1f8 100644 --- a/lang/en/reportbuilder.php +++ b/lang/en/reportbuilder.php @@ -48,11 +48,14 @@ $string['columnmoved'] = 'Moved column \'{$a}\''; $string['customreports'] = 'Custom reports'; $string['deletecolumn'] = 'Delete column \'{$a}\''; $string['deletecolumnconfirm'] = 'Are you sure you want to delete the column \'{$a}\'?'; +$string['deletefilter'] = 'Delete filter \'{$a}\''; +$string['deletefilterconfirm'] = 'Are you sure you want to delete the filter \'{$a}\'?'; $string['entitycourse'] = 'Course'; $string['entityuser'] = 'User'; $string['errorreportaccess'] = 'You can not view this report'; $string['errorsourceinvalid'] = 'Could not find valid report source'; $string['errorsourceunavailable'] = 'Report source is not available'; +$string['filteradded'] = 'Added filter \'{$a}\''; $string['filtercontains'] = 'Contains'; $string['filterdatecurrent'] = 'Current'; $string['filterdatedays'] = 'day(s)'; @@ -66,6 +69,7 @@ $string['filterdateseconds'] = 'second(s)'; $string['filterdateto'] = 'Date to'; $string['filterdateweeks'] = 'week(s)'; $string['filterdateyears'] = 'year(s)'; +$string['filterdeleted'] = 'Deleted filter \'{$a}\''; $string['filterdoesnotcontain'] = 'Does not contain'; $string['filterdurationunit'] = '{$a} unit'; $string['filterendswith'] = 'Ends with'; @@ -81,17 +85,22 @@ $string['filterisequalto'] = 'Is equal to'; $string['filterisnotempty'] = 'Is not empty'; $string['filterisnotequalto'] = 'Is not equal to'; $string['filterlessthan'] = 'Less than'; +$string['filtermoved'] = 'Moved filter \'{$a}\''; $string['filterrange'] = 'Range'; $string['filtersapplied'] = 'Filters applied'; $string['filtersappliedx'] = 'Filters ({$a})'; +$string['filters'] = 'Filters'; +$string['filters_help'] = 'Report filters allow users viewing the report to further limit the data being displayed, by selecting their own values for each filter. Filter values are stored on a per-user basis.'; $string['filtersreset'] = 'Filters reset'; $string['filterstartswith'] = 'Starts with'; $string['includedefaultsetup'] = 'Include default setup'; $string['includedefaultsetup_help'] = 'Populate report with default layout as defined by the selected source. These include pre-defined columns, filters and conditions.'; $string['movecolumn'] = 'Move column \'{$a}\''; $string['movecondition'] = 'Move condition \'{$a}\''; +$string['movefilter'] = 'Move filter \'{$a}\''; $string['newreport'] = 'New report'; $string['noconditions'] = 'There are no conditions selected'; +$string['nofilters'] = 'There are no filters selected'; $string['privacy:metadata:column'] = 'Report column definitions'; $string['privacy:metadata:column:uniqueidentifier'] = 'Unique identifier of the column'; $string['privacy:metadata:column:usercreated'] = 'The ID of the user who created the column'; @@ -106,6 +115,7 @@ $string['privacy:metadata:report:name'] = 'The name of the report'; $string['privacy:metadata:report:usercreated'] = 'The ID of the user who created the report'; $string['privacy:metadata:report:usermodified'] = 'The ID of the user who last modified the report'; $string['renamecolumn'] = 'Rename column \'{$a}\''; +$string['renamefilter'] = 'Rename filter \'{$a}\''; $string['reportbuilder'] = 'Report builder'; $string['reportcreated'] = 'Report created'; $string['reportdeleted'] = 'Report deleted'; @@ -115,6 +125,7 @@ $string['reportsource_help'] = 'The report source defines where the data for the $string['reportupdated'] = 'Report updated'; $string['resetall'] = 'Reset all'; $string['selectacondition'] = 'Select a condition'; +$string['selectafilter'] = 'Select a filter'; $string['selectareportsource'] = 'Select a report source'; $string['selectcourses'] = 'Select courses'; $string['showhide'] = 'Show/hide \'{$a}\''; diff --git a/reportbuilder/amd/build/editor.min.js b/reportbuilder/amd/build/editor.min.js index 346771930a0..f933bf89ab7 100644 --- a/reportbuilder/amd/build/editor.min.js +++ b/reportbuilder/amd/build/editor.min.js @@ -1,2 +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_reportbuilder/editor",["exports","core/inplace_editable","core/notification","core/pending","core/templates","core_reportbuilder/local/selectors","core_reportbuilder/local/editor/columns","core_reportbuilder/local/editor/conditions","core_reportbuilder/local/repository/reports"],function(a,b,c,d,e,f,g,h,i){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=void 0;c=l(c);d=l(d);e=l(e);f=k(f);function j(){if("function"!=typeof WeakMap)return null;var a=new WeakMap;j=function(){return a};return a}function k(a){if(a&&a.__esModule){return a}if(null===a||"object"!==_typeof(a)&&"function"!=typeof a){return{default:a}}var b=j();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 l(a){return a&&a.__esModule?a:{default:a}}var m=!1,n=function(){var a=document.querySelector(f.regions.report);(0,g.init)(a,m);(0,h.init)(a,m);if(m){return}document.addEventListener("click",function(b){var g=b.target.closest(f.actions.toggleEditPreview);if(g){b.preventDefault();var h=new d.default("core_reportbuilder/reports:get"),j="1"!==g.dataset.editMode,k="";(0,i.getReport)(a.dataset.reportId,j).then(function(a){k=a.javascript;return e.default.render("core_reportbuilder/custom_report",a)}).then(function(b,c){return e.default.replaceNodeContents(a,b,c+k)}).then(function(){return h.resolve()}).catch(c.default.exception)}});m=!0};a.init=n}); +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_reportbuilder/editor",["exports","core/inplace_editable","core/notification","core/pending","core/templates","core_reportbuilder/local/selectors","core_reportbuilder/local/editor/columns","core_reportbuilder/local/editor/conditions","core_reportbuilder/local/editor/filters","core_reportbuilder/local/repository/reports"],function(a,b,c,d,e,f,g,h,i,j){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=void 0;c=m(c);d=m(d);e=m(e);f=l(f);function k(){if("function"!=typeof WeakMap)return null;var a=new WeakMap;k=function(){return a};return a}function l(a){if(a&&a.__esModule){return a}if(null===a||"object"!==_typeof(a)&&"function"!=typeof a){return{default:a}}var b=k();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 m(a){return a&&a.__esModule?a:{default:a}}var n=!1,o=function(){var a=document.querySelector(f.regions.report);(0,g.init)(a,n);(0,h.init)(a,n);(0,i.init)(a,n);if(n){return}document.addEventListener("click",function(b){var g=b.target.closest(f.actions.toggleEditPreview);if(g){b.preventDefault();var h=new d.default("core_reportbuilder/reports:get"),i="1"!==g.dataset.editMode,k="";(0,j.getReport)(a.dataset.reportId,i).then(function(a){k=a.javascript;return e.default.render("core_reportbuilder/custom_report",a)}).then(function(b,c){return e.default.replaceNodeContents(a,b,c+k)}).then(function(){return h.resolve()}).catch(c.default.exception)}});n=!0};a.init=o}); //# sourceMappingURL=editor.min.js.map diff --git a/reportbuilder/amd/build/editor.min.js.map b/reportbuilder/amd/build/editor.min.js.map index 02b797c59a0..e863012e64f 100644 --- a/reportbuilder/amd/build/editor.min.js.map +++ b/reportbuilder/amd/build/editor.min.js.map @@ -1 +1 @@ -{"version":3,"sources":["../src/editor.js"],"names":["initialized","init","reportElement","document","querySelector","reportSelectors","regions","report","addEventListener","event","toggleEditViewMode","target","closest","actions","toggleEditPreview","preventDefault","pendingPromise","Pending","toggledEditMode","dataset","editMode","customjs","reportId","then","response","javascript","Templates","render","html","js","replaceNodeContents","resolve","catch","Notification","exception"],"mappings":"qmBAwBA,a,+DAGA,OACA,OACA,OACA,O,4lBAKIA,CAAAA,CAAW,G,CAKFC,CAAI,CAAG,UAAM,CACtB,GAAMC,CAAAA,CAAa,CAAGC,QAAQ,CAACC,aAAT,CAAuBC,CAAe,CAACC,OAAhB,CAAwBC,MAA/C,CAAtB,CAEA,WAAkBL,CAAlB,CAAiCF,CAAjC,EACA,WAAqBE,CAArB,CAAoCF,CAApC,EAGA,GAAIA,CAAJ,CAAiB,CACb,MACH,CAGDG,QAAQ,CAACK,gBAAT,CAA0B,OAA1B,CAAmC,SAAAC,CAAK,CAAI,CAGxC,GAAMC,CAAAA,CAAkB,CAAGD,CAAK,CAACE,MAAN,CAAaC,OAAb,CAAqBP,CAAe,CAACQ,OAAhB,CAAwBC,iBAA7C,CAA3B,CACA,GAAIJ,CAAJ,CAAwB,CACpBD,CAAK,CAACM,cAAN,GADoB,GAGdC,CAAAA,CAAc,CAAG,GAAIC,UAAJ,CAAY,gCAAZ,CAHH,CAIdC,CAAe,CAA2C,GAAxC,GAAAR,CAAkB,CAACS,OAAnB,CAA2BC,QAJ/B,CAMhBC,CAAQ,CAAG,EANK,CAQpB,gBAAUnB,CAAa,CAACiB,OAAd,CAAsBG,QAAhC,CAA0CJ,CAA1C,EACKK,IADL,CACU,SAAAC,CAAQ,CAAI,CACdH,CAAQ,CAAGG,CAAQ,CAACC,UAApB,CACA,MAAOC,WAAUC,MAAV,CAAiB,kCAAjB,CAAqDH,CAArD,CACV,CAJL,EAKKD,IALL,CAKU,SAACK,CAAD,CAAOC,CAAP,CAAc,CAChB,MAAOH,WAAUI,mBAAV,CAA8B5B,CAA9B,CAA6C0B,CAA7C,CAAmDC,CAAE,CAAGR,CAAxD,CACV,CAPL,EAQKE,IARL,CAQU,iBAAMP,CAAAA,CAAc,CAACe,OAAf,EAAN,CARV,EASKC,KATL,CASWC,UAAaC,SATxB,CAUH,CACJ,CAvBD,EAyBAlC,CAAW,GACd,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 * Report builder editor\n *\n * @module core_reportbuilder/editor\n * @package core_reportbuilder\n * @copyright 2021 David Matamoros \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\"use strict\";\n\nimport 'core/inplace_editable';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\nimport Templates from 'core/templates';\nimport * as reportSelectors from 'core_reportbuilder/local/selectors';\nimport {init as columnsEditorInit} from 'core_reportbuilder/local/editor/columns';\nimport {init as conditionsEditorInit} from 'core_reportbuilder/local/editor/conditions';\nimport {getReport} from 'core_reportbuilder/local/repository/reports';\n\nlet initialized = false;\n\n/**\n * Initialise editor and all it's modules\n */\nexport const init = () => {\n const reportElement = document.querySelector(reportSelectors.regions.report);\n\n columnsEditorInit(reportElement, initialized);\n conditionsEditorInit(reportElement, initialized);\n\n // Ensure we only add our listeners once (can be called multiple times by mustache template).\n if (initialized) {\n return;\n }\n\n // Add event handlers to generic report editor elements.\n document.addEventListener('click', event => {\n\n // Toggle between edit and preview mode.\n const toggleEditViewMode = event.target.closest(reportSelectors.actions.toggleEditPreview);\n if (toggleEditViewMode) {\n event.preventDefault();\n\n const pendingPromise = new Pending('core_reportbuilder/reports:get');\n const toggledEditMode = toggleEditViewMode.dataset.editMode !== \"1\";\n\n let customjs = '';\n\n getReport(reportElement.dataset.reportId, toggledEditMode)\n .then(response => {\n customjs = response.javascript;\n return Templates.render('core_reportbuilder/custom_report', response);\n })\n .then((html, js) => {\n return Templates.replaceNodeContents(reportElement, html, js + customjs);\n })\n .then(() => pendingPromise.resolve())\n .catch(Notification.exception);\n }\n });\n\n initialized = true;\n};\n"],"file":"editor.min.js"} \ No newline at end of file +{"version":3,"sources":["../src/editor.js"],"names":["initialized","init","reportElement","document","querySelector","reportSelectors","regions","report","addEventListener","event","toggleEditViewMode","target","closest","actions","toggleEditPreview","preventDefault","pendingPromise","Pending","toggledEditMode","dataset","editMode","customjs","reportId","then","response","javascript","Templates","render","html","js","replaceNodeContents","resolve","catch","Notification","exception"],"mappings":"ipBAwBA,a,+DAGA,OACA,OACA,OACA,O,4lBAMIA,CAAAA,CAAW,G,CAKFC,CAAI,CAAG,UAAM,CACtB,GAAMC,CAAAA,CAAa,CAAGC,QAAQ,CAACC,aAAT,CAAuBC,CAAe,CAACC,OAAhB,CAAwBC,MAA/C,CAAtB,CAEA,WAAkBL,CAAlB,CAAiCF,CAAjC,EACA,WAAqBE,CAArB,CAAoCF,CAApC,EACA,WAAkBE,CAAlB,CAAiCF,CAAjC,EAGA,GAAIA,CAAJ,CAAiB,CACb,MACH,CAGDG,QAAQ,CAACK,gBAAT,CAA0B,OAA1B,CAAmC,SAAAC,CAAK,CAAI,CAGxC,GAAMC,CAAAA,CAAkB,CAAGD,CAAK,CAACE,MAAN,CAAaC,OAAb,CAAqBP,CAAe,CAACQ,OAAhB,CAAwBC,iBAA7C,CAA3B,CACA,GAAIJ,CAAJ,CAAwB,CACpBD,CAAK,CAACM,cAAN,GADoB,GAGdC,CAAAA,CAAc,CAAG,GAAIC,UAAJ,CAAY,gCAAZ,CAHH,CAIdC,CAAe,CAA2C,GAAxC,GAAAR,CAAkB,CAACS,OAAnB,CAA2BC,QAJ/B,CAMhBC,CAAQ,CAAG,EANK,CAQpB,gBAAUnB,CAAa,CAACiB,OAAd,CAAsBG,QAAhC,CAA0CJ,CAA1C,EACKK,IADL,CACU,SAAAC,CAAQ,CAAI,CACdH,CAAQ,CAAGG,CAAQ,CAACC,UAApB,CACA,MAAOC,WAAUC,MAAV,CAAiB,kCAAjB,CAAqDH,CAArD,CACV,CAJL,EAKKD,IALL,CAKU,SAACK,CAAD,CAAOC,CAAP,CAAc,CAChB,MAAOH,WAAUI,mBAAV,CAA8B5B,CAA9B,CAA6C0B,CAA7C,CAAmDC,CAAE,CAAGR,CAAxD,CACV,CAPL,EAQKE,IARL,CAQU,iBAAMP,CAAAA,CAAc,CAACe,OAAf,EAAN,CARV,EASKC,KATL,CASWC,UAAaC,SATxB,CAUH,CACJ,CAvBD,EAyBAlC,CAAW,GACd,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 * Report builder editor\n *\n * @module core_reportbuilder/editor\n * @package core_reportbuilder\n * @copyright 2021 David Matamoros \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\"use strict\";\n\nimport 'core/inplace_editable';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\nimport Templates from 'core/templates';\nimport * as reportSelectors from 'core_reportbuilder/local/selectors';\nimport {init as columnsEditorInit} from 'core_reportbuilder/local/editor/columns';\nimport {init as conditionsEditorInit} from 'core_reportbuilder/local/editor/conditions';\nimport {init as filtersEditorInit} from 'core_reportbuilder/local/editor/filters';\nimport {getReport} from 'core_reportbuilder/local/repository/reports';\n\nlet initialized = false;\n\n/**\n * Initialise editor and all it's modules\n */\nexport const init = () => {\n const reportElement = document.querySelector(reportSelectors.regions.report);\n\n columnsEditorInit(reportElement, initialized);\n conditionsEditorInit(reportElement, initialized);\n filtersEditorInit(reportElement, initialized);\n\n // Ensure we only add our listeners once (can be called multiple times by mustache template).\n if (initialized) {\n return;\n }\n\n // Add event handlers to generic report editor elements.\n document.addEventListener('click', event => {\n\n // Toggle between edit and preview mode.\n const toggleEditViewMode = event.target.closest(reportSelectors.actions.toggleEditPreview);\n if (toggleEditViewMode) {\n event.preventDefault();\n\n const pendingPromise = new Pending('core_reportbuilder/reports:get');\n const toggledEditMode = toggleEditViewMode.dataset.editMode !== \"1\";\n\n let customjs = '';\n\n getReport(reportElement.dataset.reportId, toggledEditMode)\n .then(response => {\n customjs = response.javascript;\n return Templates.render('core_reportbuilder/custom_report', response);\n })\n .then((html, js) => {\n return Templates.replaceNodeContents(reportElement, html, js + customjs);\n })\n .then(() => pendingPromise.resolve())\n .catch(Notification.exception);\n }\n });\n\n initialized = true;\n};\n"],"file":"editor.min.js"} \ No newline at end of file diff --git a/reportbuilder/amd/build/local/editor/filters.min.js b/reportbuilder/amd/build/local/editor/filters.min.js new file mode 100644 index 00000000000..94073c6165b --- /dev/null +++ b/reportbuilder/amd/build/local/editor/filters.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_reportbuilder/local/editor/filters",["exports","jquery","core/event_dispatcher","core/inplace_editable","core/notification","core/pending","core/sortable_list","core/str","core/templates","core/toast","core_reportbuilder/local/events","core_reportbuilder/local/selectors","core_reportbuilder/local/repository/filters"],function(a,b,c,d,e,f,g,h,i,j,k,l,m){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=void 0;b=p(b);e=p(e);f=p(f);g=p(g);i=p(i);k=o(k);l=o(l);function n(){if("function"!=typeof WeakMap)return null;var a=new WeakMap;n=function(){return a};return a}function o(a){if(a&&a.__esModule){return a}if(null===a||"object"!==_typeof(a)&&"function"!=typeof a){return{default:a}}var b=n();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 p(a){return a&&a.__esModule?a:{default:a}}function q(a,b){return v(a)||u(a,b)||s(a,b)||r()}function r(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function s(a,b){if(!a)return;if("string"==typeof a)return t(a,b);var c=Object.prototype.toString.call(a).slice(8,-1);if("Object"===c&&a.constructor)c=a.constructor.name;if("Map"===c||"Set"===c)return Array.from(c);if("Arguments"===c||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(c))return t(a,b)}function t(a,b){if(null==b||b>a.length)b=a.length;for(var c=0,d=Array(b);ci){k--}(0,m.reorderFilter)(a.dataset.reportId,g,k).then(function(b){return w(a,b)}).then(function(){return(0,h.get_string)("filtermoved","core_reportbuilder",c.element.data("filterName"))}).then(j.add).then(function(){return d.resolve()}).catch(e.default.exception)}})};a.init=x}); +//# sourceMappingURL=filters.min.js.map diff --git a/reportbuilder/amd/build/local/editor/filters.min.js.map b/reportbuilder/amd/build/local/editor/filters.min.js.map new file mode 100644 index 00000000000..b1c860b2061 --- /dev/null +++ b/reportbuilder/amd/build/local/editor/filters.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../../src/local/editor/filters.js"],"names":["reloadSettingsFiltersRegion","reportElement","templateContext","pendingPromise","Pending","settingsFiltersRegion","querySelector","reportSelectors","regions","settingsFilters","Templates","renderForPromise","filters","then","html","js","replaceNode","resolve","init","initialized","addEventListener","event","reportAddFilter","target","closest","actions","preventDefault","value","dataset","reportId","data","options","selectedIndex","text","addToast","catch","Notification","exception","reportRemoveFilter","filterContainer","activeFilter","filterName","key","component","param","confirmTitle","confirmText","confirmButton","confirm","filterId","reportEvents","tableReload","activeFiltersSortableList","SortableList","activeFilters","isHorizontal","getElementName","element","Promise","on","EVENTS","DROP","info","positionChanged","filterPosition","targetFilterPosition","targetNextElement","siblings","length"],"mappings":"opBAwBA,a,+DAEA,OAGA,OACA,OACA,OAEA,OAEA,OACA,O,wjDAUMA,CAAAA,CAA2B,CAAG,SAACC,CAAD,CAAgBC,CAAhB,CAAoC,IAC9DC,CAAAA,CAAc,CAAG,GAAIC,UAAJ,CAAY,mCAAZ,CAD6C,CAE9DC,CAAqB,CAAGJ,CAAa,CAACK,aAAd,CAA4BC,CAAe,CAACC,OAAhB,CAAwBC,eAApD,CAFsC,CAIpE,MAAOC,WAAUC,gBAAV,CAA2B,2CAA3B,CAAwE,CAACC,OAAO,CAAEV,CAAV,CAAxE,EACFW,IADE,CACG,WAAgB,IAAdC,CAAAA,CAAc,GAAdA,IAAc,CAARC,CAAQ,GAARA,EAAQ,CAClBL,UAAUM,WAAV,CAAsBX,CAAtB,CAA6CS,CAA7C,CAAmDC,CAAnD,EACA,MAAOZ,CAAAA,CAAc,CAACc,OAAf,EACV,CAJE,CAKV,C,CAQYC,CAAI,CAAG,SAACjB,CAAD,CAAgBkB,CAAhB,CAAgC,CAChD,GAAIA,CAAJ,CAAiB,CACb,MACH,CAEDlB,CAAa,CAACmB,gBAAd,CAA+B,OAA/B,CAAwC,SAAAC,CAAK,CAAI,CAG7C,GAAMC,CAAAA,CAAe,CAAGD,CAAK,CAACE,MAAN,CAAaC,OAAb,CAAqBjB,CAAe,CAACkB,OAAhB,CAAwBH,eAA7C,CAAxB,CACA,GAAIA,CAAJ,CAAqB,CACjBD,CAAK,CAACK,cAAN,GAGA,GAA8B,GAA1B,GAAAJ,CAAe,CAACK,KAApB,CAAmC,CAC/B,MACH,CAED,GAAMxB,CAAAA,CAAc,CAAG,GAAIC,UAAJ,CAAY,gCAAZ,CAAvB,CAEA,gBAAUH,CAAa,CAAC2B,OAAd,CAAsBC,QAAhC,CAA0CP,CAAe,CAACK,KAA1D,EACKd,IADL,CACU,SAAAiB,CAAI,QAAI9B,CAAAA,CAA2B,CAACC,CAAD,CAAgB6B,CAAhB,CAA/B,CADd,EAEKjB,IAFL,CAEU,iBAAM,iBAAU,aAAV,CAAyB,oBAAzB,CACRS,CAAe,CAACS,OAAhB,CAAwBT,CAAe,CAACU,aAAxC,EAAuDC,IAD/C,CAAN,CAFV,EAIKpB,IAJL,CAIUqB,KAJV,EAKKrB,IALL,CAKU,iBAAMV,CAAAA,CAAc,CAACc,OAAf,EAAN,CALV,EAMKkB,KANL,CAMWC,UAAaC,SANxB,CAOH,CAGD,GAAMC,CAAAA,CAAkB,CAAGjB,CAAK,CAACE,MAAN,CAAaC,OAAb,CAAqBjB,CAAe,CAACkB,OAAhB,CAAwBa,kBAA7C,CAA3B,CACA,GAAIA,CAAJ,CAAwB,CACpBjB,CAAK,CAACK,cAAN,GADoB,GAGda,CAAAA,CAAe,CAAGD,CAAkB,CAACd,OAAnB,CAA2BjB,CAAe,CAACC,OAAhB,CAAwBgC,YAAnD,CAHJ,CAIdC,CAAU,CAAGF,CAAe,CAACX,OAAhB,CAAwBa,UAJvB,CAMpB,kBAAW,CACP,CAACC,GAAG,CAAE,cAAN,CAAsBC,SAAS,CAAE,oBAAjC,CAAuDC,KAAK,CAAEH,CAA9D,CADO,CAEP,CAACC,GAAG,CAAE,qBAAN,CAA6BC,SAAS,CAAE,oBAAxC,CAA8DC,KAAK,CAAEH,CAArE,CAFO,CAGP,CAACC,GAAG,CAAE,QAAN,CAAgBC,SAAS,CAAE,QAA3B,CAHO,CAAX,EAIG9B,IAJH,CAIQ,WAAgD,cAA9CgC,CAA8C,MAAhCC,CAAgC,MAAnBC,CAAmB,MACpDX,UAAaY,OAAb,CAAqBH,CAArB,CAAmCC,CAAnC,CAAgDC,CAAhD,CAA+D,IAA/D,CAAqE,UAAM,CACvE,GAAM5C,CAAAA,CAAc,CAAG,GAAIC,UAAJ,CAAY,mCAAZ,CAAvB,CAEA,mBAAaH,CAAa,CAAC2B,OAAd,CAAsBC,QAAnC,CAA6CU,CAAe,CAACX,OAAhB,CAAwBqB,QAArE,EACKpC,IADL,CACU,SAAAiB,CAAI,QAAI9B,CAAAA,CAA2B,CAACC,CAAD,CAAgB6B,CAAhB,CAA/B,CADd,EAEKjB,IAFL,CAEU,iBAAM,iBAAU,eAAV,CAA2B,oBAA3B,CAAiD4B,CAAjD,CAAN,CAFV,EAGK5B,IAHL,CAGUqB,KAHV,EAIKrB,IAJL,CAIU,UAAM,CACR,oBAAcqC,CAAY,CAACC,WAA3B,CAAwC,EAAxC,CAA4ClD,CAA5C,EACA,MAAOE,CAAAA,CAAc,CAACc,OAAf,EACV,CAPL,EAQKkB,KARL,CAQWC,UAAaC,SARxB,CASH,CAZD,CAcH,CAnBD,EAmBGF,KAnBH,CAmBSC,UAAaC,SAnBtB,CAoBH,CACJ,CApDD,EAuDA,GAAIe,CAAAA,CAAyB,CAAG,GAAIC,UAAJ,WAAoB9C,CAAe,CAACC,OAAhB,CAAwB8C,aAA5C,QAAgE,CAACC,YAAY,GAAb,CAAhE,CAAhC,CACAH,CAAyB,CAACI,cAA1B,CAA2C,SAAAC,CAAO,QAAIC,CAAAA,OAAO,CAACzC,OAAR,CAAgBwC,CAAO,CAAC3B,IAAR,CAAa,YAAb,CAAhB,CAAJ,CAAlD,CAEA,cAAE7B,CAAF,EAAiB0D,EAAjB,CAAoBN,UAAaO,MAAb,CAAoBC,IAAxC,CAA8C,oBAA9C,CAAoE,SAACxC,CAAD,CAAQyC,CAAR,CAAiB,CACjF,GAAIA,CAAI,CAACC,eAAT,CAA0B,IAChB5D,CAAAA,CAAc,CAAG,GAAIC,UAAJ,CAAY,oCAAZ,CADD,CAEhB6C,CAAQ,CAAGa,CAAI,CAACL,OAAL,CAAa3B,IAAb,CAAkB,UAAlB,CAFK,CAGhBkC,CAAc,CAAGF,CAAI,CAACL,OAAL,CAAa3B,IAAb,CAAkB,gBAAlB,CAHD,CAMlBmC,CAAoB,CAAGH,CAAI,CAACI,iBAAL,CAAuBpC,IAAvB,CAA4B,gBAA5B,GAAiDgC,CAAI,CAACL,OAAL,CAAaU,QAAb,GAAwBC,MAAxB,CAAiC,CANvF,CAOtB,GAAIH,CAAoB,CAAGD,CAA3B,CAA2C,CACvCC,CAAoB,EACvB,CAED,oBAAchE,CAAa,CAAC2B,OAAd,CAAsBC,QAApC,CAA8CoB,CAA9C,CAAwDgB,CAAxD,EACKpD,IADL,CACU,SAAAiB,CAAI,QAAI9B,CAAAA,CAA2B,CAACC,CAAD,CAAgB6B,CAAhB,CAA/B,CADd,EAEKjB,IAFL,CAEU,iBAAM,iBAAU,aAAV,CAAyB,oBAAzB,CAA+CiD,CAAI,CAACL,OAAL,CAAa3B,IAAb,CAAkB,YAAlB,CAA/C,CAAN,CAFV,EAGKjB,IAHL,CAGUqB,KAHV,EAIKrB,IAJL,CAIU,iBAAMV,CAAAA,CAAc,CAACc,OAAf,EAAN,CAJV,EAKKkB,KALL,CAKWC,UAAaC,SALxB,CAMH,CACJ,CAnBD,CAoBH,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 * Report builder filters editor\n *\n * @module core_reportbuilder/local/editor/filters\n * @package core_reportbuilder\n * @copyright 2021 David Matamoros \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\"use strict\";\n\nimport $ from 'jquery';\nimport {dispatchEvent} from 'core/event_dispatcher';\nimport 'core/inplace_editable';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\nimport SortableList from 'core/sortable_list';\nimport {get_string as getString, get_strings as getStrings} from 'core/str';\nimport Templates from 'core/templates';\nimport {add as addToast} from 'core/toast';\nimport * as reportEvents from 'core_reportbuilder/local/events';\nimport * as reportSelectors from 'core_reportbuilder/local/selectors';\nimport {addFilter, deleteFilter, reorderFilter} from 'core_reportbuilder/local/repository/filters';\n\n/**\n * Reload filters settings region\n *\n * @param {Element} reportElement\n * @param {Object} templateContext\n * @return {Promise}\n */\nconst reloadSettingsFiltersRegion = (reportElement, templateContext) => {\n const pendingPromise = new Pending('core_reportbuilder/filters:reload');\n const settingsFiltersRegion = reportElement.querySelector(reportSelectors.regions.settingsFilters);\n\n return Templates.renderForPromise('core_reportbuilder/local/settings/filters', {filters: templateContext})\n .then(({html, js}) => {\n Templates.replaceNode(settingsFiltersRegion, html, js);\n return pendingPromise.resolve();\n });\n};\n\n/**\n * Initialise module\n *\n * @param {Element} reportElement\n * @param {Boolean} initialized Ensure we only add our listeners once\n */\nexport const init = (reportElement, initialized) => {\n if (initialized) {\n return;\n }\n\n reportElement.addEventListener('click', event => {\n\n // Add filter to report.\n const reportAddFilter = event.target.closest(reportSelectors.actions.reportAddFilter);\n if (reportAddFilter) {\n event.preventDefault();\n\n // Check if dropdown is closed with no filter selected.\n if (reportAddFilter.value === '0') {\n return;\n }\n\n const pendingPromise = new Pending('core_reportbuilder/filters:add');\n\n addFilter(reportElement.dataset.reportId, reportAddFilter.value)\n .then(data => reloadSettingsFiltersRegion(reportElement, data))\n .then(() => getString('filteradded', 'core_reportbuilder',\n reportAddFilter.options[reportAddFilter.selectedIndex].text))\n .then(addToast)\n .then(() => pendingPromise.resolve())\n .catch(Notification.exception);\n }\n\n // Remove filter from report.\n const reportRemoveFilter = event.target.closest(reportSelectors.actions.reportRemoveFilter);\n if (reportRemoveFilter) {\n event.preventDefault();\n\n const filterContainer = reportRemoveFilter.closest(reportSelectors.regions.activeFilter);\n const filterName = filterContainer.dataset.filterName;\n\n getStrings([\n {key: 'deletefilter', component: 'core_reportbuilder', param: filterName},\n {key: 'deletefilterconfirm', component: 'core_reportbuilder', param: filterName},\n {key: 'delete', component: 'moodle'},\n ]).then(([confirmTitle, confirmText, confirmButton]) => {\n Notification.confirm(confirmTitle, confirmText, confirmButton, null, () => {\n const pendingPromise = new Pending('core_reportbuilder/filters:remove');\n\n deleteFilter(reportElement.dataset.reportId, filterContainer.dataset.filterId)\n .then(data => reloadSettingsFiltersRegion(reportElement, data))\n .then(() => getString('filterdeleted', 'core_reportbuilder', filterName))\n .then(addToast)\n .then(() => {\n dispatchEvent(reportEvents.tableReload, {}, reportElement);\n return pendingPromise.resolve();\n })\n .catch(Notification.exception);\n });\n return;\n }).catch(Notification.exception);\n }\n });\n\n // Initialize sortable list to handle active filters moving (note JQuery dependency, see MDL-72293 for resolution).\n var activeFiltersSortableList = new SortableList(`${reportSelectors.regions.activeFilters} ul`, {isHorizontal: false});\n activeFiltersSortableList.getElementName = element => Promise.resolve(element.data('filterName'));\n\n $(reportElement).on(SortableList.EVENTS.DROP, 'li[data-filter-id]', (event, info) => {\n if (info.positionChanged) {\n const pendingPromise = new Pending('core_reportbuilder/filters:reorder');\n const filterId = info.element.data('filterId');\n const filterPosition = info.element.data('filterPosition');\n\n // Select target position, if moving to the end then count number of element siblings.\n let targetFilterPosition = info.targetNextElement.data('filterPosition') || info.element.siblings().length + 2;\n if (targetFilterPosition > filterPosition) {\n targetFilterPosition--;\n }\n\n reorderFilter(reportElement.dataset.reportId, filterId, targetFilterPosition)\n .then(data => reloadSettingsFiltersRegion(reportElement, data))\n .then(() => getString('filtermoved', 'core_reportbuilder', info.element.data('filterName')))\n .then(addToast)\n .then(() => pendingPromise.resolve())\n .catch(Notification.exception);\n }\n });\n};\n"],"file":"filters.min.js"} \ No newline at end of file diff --git a/reportbuilder/amd/build/local/repository/filters.min.js b/reportbuilder/amd/build/local/repository/filters.min.js index e1c7f17cc27..765fa025c0f 100644 --- a/reportbuilder/amd/build/local/repository/filters.min.js +++ b/reportbuilder/amd/build/local/repository/filters.min.js @@ -1,2 +1,2 @@ -define ("core_reportbuilder/local/repository/filters",["exports","core/ajax"],function(a,b){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.resetFilters=void 0;b=function(a){return a&&a.__esModule?a:{default:a}}(b);var c=function(a){return b.default.call([{methodname:"core_reportbuilder_filters_reset",args:{reportid:a}}])[0]};a.resetFilters=c}); +define ("core_reportbuilder/local/repository/filters",["exports","core/ajax"],function(a,b){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.reorderFilter=a.deleteFilter=a.addFilter=a.resetFilters=void 0;b=function(a){return a&&a.__esModule?a:{default:a}}(b);var c=function(a){return b.default.call([{methodname:"core_reportbuilder_filters_reset",args:{reportid:a}}])[0]};a.resetFilters=c;var d=function(a,c){return b.default.call([{methodname:"core_reportbuilder_filters_add",args:{reportid:a,uniqueidentifier:c}}])[0]};a.addFilter=d;var e=function(a,c){return b.default.call([{methodname:"core_reportbuilder_filters_delete",args:{reportid:a,filterid:c}}])[0]};a.deleteFilter=e;var f=function(a,c,d){return b.default.call([{methodname:"core_reportbuilder_filters_reorder",args:{reportid:a,filterid:c,position:d}}])[0]};a.reorderFilter=f}); //# sourceMappingURL=filters.min.js.map diff --git a/reportbuilder/amd/build/local/repository/filters.min.js.map b/reportbuilder/amd/build/local/repository/filters.min.js.map index af457d5f121..5527b51ec96 100644 --- a/reportbuilder/amd/build/local/repository/filters.min.js.map +++ b/reportbuilder/amd/build/local/repository/filters.min.js.map @@ -1 +1 @@ -{"version":3,"sources":["../../../src/local/repository/filters.js"],"names":["resetFilters","reportId","Ajax","call","methodname","args","reportid"],"mappings":"gLAuBA,uDASO,GAAMA,CAAAA,CAAY,CAAG,SAAAC,CAAQ,CAAI,CAMpC,MAAOC,WAAKC,IAAL,CAAU,CALD,CACZC,UAAU,CAAE,kCADA,CAEZC,IAAI,CAAE,CAACC,QAAQ,CAAEL,CAAX,CAFM,CAKC,CAAV,EAAqB,CAArB,CACV,CAPM,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 to handle filter AJAX requests\n *\n * @module core_reportbuilder/local/repository/filters\n * @copyright 2021 Paul Holden \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\n\n/**\n * Reset all filters for given report\n *\n * @method\n * @param {Number} reportId\n * @return {Promise}\n */\nexport const resetFilters = reportId => {\n const request = {\n methodname: 'core_reportbuilder_filters_reset',\n args: {reportid: reportId}\n };\n\n return Ajax.call([request])[0];\n};\n"],"file":"filters.min.js"} \ No newline at end of file +{"version":3,"sources":["../../../src/local/repository/filters.js"],"names":["resetFilters","reportId","Ajax","call","methodname","args","reportid","addFilter","uniqueIdentifier","uniqueidentifier","deleteFilter","filterId","filterid","reorderFilter","position"],"mappings":"2NAuBA,uDASO,GAAMA,CAAAA,CAAY,CAAG,SAAAC,CAAQ,CAAI,CAMpC,MAAOC,WAAKC,IAAL,CAAU,CALD,CACZC,UAAU,CAAE,kCADA,CAEZC,IAAI,CAAE,CAACC,QAAQ,CAAEL,CAAX,CAFM,CAKC,CAAV,EAAqB,CAArB,CACV,CAPM,C,iBAgBA,GAAMM,CAAAA,CAAS,CAAG,SAACN,CAAD,CAAWO,CAAX,CAAgC,CAMrD,MAAON,WAAKC,IAAL,CAAU,CALD,CACZC,UAAU,CAAE,gCADA,CAEZC,IAAI,CAAE,CAACC,QAAQ,CAAEL,CAAX,CAAqBQ,gBAAgB,CAAED,CAAvC,CAFM,CAKC,CAAV,EAAqB,CAArB,CACV,CAPM,C,cAgBA,GAAME,CAAAA,CAAY,CAAG,SAACT,CAAD,CAAWU,CAAX,CAAwB,CAMhD,MAAOT,WAAKC,IAAL,CAAU,CALD,CACZC,UAAU,CAAE,mCADA,CAEZC,IAAI,CAAE,CAACC,QAAQ,CAAEL,CAAX,CAAqBW,QAAQ,CAAED,CAA/B,CAFM,CAKC,CAAV,EAAqB,CAArB,CACV,CAPM,C,iBAiBA,GAAME,CAAAA,CAAa,CAAG,SAACZ,CAAD,CAAWU,CAAX,CAAqBG,CAArB,CAAkC,CAM3D,MAAOZ,WAAKC,IAAL,CAAU,CALD,CACZC,UAAU,CAAE,oCADA,CAEZC,IAAI,CAAE,CAACC,QAAQ,CAAEL,CAAX,CAAqBW,QAAQ,CAAED,CAA/B,CAAyCG,QAAQ,CAAEA,CAAnD,CAFM,CAKC,CAAV,EAAqB,CAArB,CACV,CAPM,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 to handle filter AJAX requests\n *\n * @module core_reportbuilder/local/repository/filters\n * @copyright 2021 Paul Holden \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\n\n/**\n * Reset all filters for given report\n *\n * @method\n * @param {Number} reportId\n * @return {Promise}\n */\nexport const resetFilters = reportId => {\n const request = {\n methodname: 'core_reportbuilder_filters_reset',\n args: {reportid: reportId}\n };\n\n return Ajax.call([request])[0];\n};\n\n/**\n * Add a filter to the given report\n *\n * @param {Number} reportId\n * @param {String} uniqueIdentifier\n * @return {Promise}\n */\nexport const addFilter = (reportId, uniqueIdentifier) => {\n const request = {\n methodname: 'core_reportbuilder_filters_add',\n args: {reportid: reportId, uniqueidentifier: uniqueIdentifier}\n };\n\n return Ajax.call([request])[0];\n};\n\n/**\n * Remove filter from given report\n *\n * @param {Number} reportId\n * @param {Number} filterId\n * @return {Promise}\n */\nexport const deleteFilter = (reportId, filterId) => {\n const request = {\n methodname: 'core_reportbuilder_filters_delete',\n args: {reportid: reportId, filterid: filterId}\n };\n\n return Ajax.call([request])[0];\n};\n\n/**\n * Reorder a filter in a given report\n *\n * @param {Number} reportId\n * @param {Number} filterId\n * @param {Number} position\n * @return {Promise}\n */\nexport const reorderFilter = (reportId, filterId, position) => {\n const request = {\n methodname: 'core_reportbuilder_filters_reorder',\n args: {reportid: reportId, filterid: filterId, position: position}\n };\n\n return Ajax.call([request])[0];\n};\n"],"file":"filters.min.js"} \ No newline at end of file diff --git a/reportbuilder/amd/build/local/selectors.min.js b/reportbuilder/amd/build/local/selectors.min.js index 528108ddb36..d0aae51481c 100644 --- a/reportbuilder/amd/build/local/selectors.min.js +++ b/reportbuilder/amd/build/local/selectors.min.js @@ -1,2 +1,2 @@ -define ("core_reportbuilder/local/selectors",["exports"],function(a){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;var b={regions:{report:"[data-region=\"core_reportbuilder/report\"]",reportTable:"[data-region=\"reportbuilder-table\"]",columnHeader:"[data-region=\"column-header\"]",filterButtonLabel:"[data-region=\"filter-button-label\"]",filtersForm:"[data-region=\"filters-form\"]",sidebarMenu:"[data-region=\"sidebar-menu\"]",sidebarCard:"[data-region=\"sidebar-card\"]",settingsConditions:"[data-region=\"settings-conditions\"]",activeConditions:"[data-region=\"active-conditions\"]",activeCondition:"[data-region=\"active-condition\"]"},actions:{reportActionPopup:"[data-action=\"report-action-popup\"]",reportCreate:"[data-action=\"report-create\"]",reportEdit:"[data-action=\"report-edit\"]",reportDelete:"[data-action=\"report-delete\"]",reportAddColumn:"[data-action=\"report-add-column\"]",reportRemoveColumn:"[data-action=\"report-remove-column\"]",reportAddCondition:"[data-action=\"report-add-condition\"]",reportRemoveCondition:"[data-action=\"report-remove-condition\"]",sidebarSearch:"[data-action=\"sidebar-search\"]"}};b.forReport=function(a){return"".concat(b.regions.report,"[data-report-id=\"").concat(a,"\"]")};a.default=b;return a.default}); +define ("core_reportbuilder/local/selectors",["exports"],function(a){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;var b={regions:{report:"[data-region=\"core_reportbuilder/report\"]",reportTable:"[data-region=\"reportbuilder-table\"]",columnHeader:"[data-region=\"column-header\"]",filterButtonLabel:"[data-region=\"filter-button-label\"]",filtersForm:"[data-region=\"filters-form\"]",sidebarMenu:"[data-region=\"sidebar-menu\"]",sidebarCard:"[data-region=\"sidebar-card\"]",settingsConditions:"[data-region=\"settings-conditions\"]",activeConditions:"[data-region=\"active-conditions\"]",activeCondition:"[data-region=\"active-condition\"]",settingsFilters:"[data-region=\"settings-filters\"]",activeFilters:"[data-region=\"active-filters\"]",activeFilter:"[data-region=\"active-filter\"]"},actions:{reportActionPopup:"[data-action=\"report-action-popup\"]",reportCreate:"[data-action=\"report-create\"]",reportEdit:"[data-action=\"report-edit\"]",reportDelete:"[data-action=\"report-delete\"]",reportAddColumn:"[data-action=\"report-add-column\"]",reportRemoveColumn:"[data-action=\"report-remove-column\"]",reportAddCondition:"[data-action=\"report-add-condition\"]",reportRemoveCondition:"[data-action=\"report-remove-condition\"]",reportAddFilter:"[data-action=\"report-add-filter\"]",reportRemoveFilter:"[data-action=\"report-remove-filter\"]",sidebarSearch:"[data-action=\"sidebar-search\"]"}};b.forReport=function(a){return"".concat(b.regions.report,"[data-report-id=\"").concat(a,"\"]")};a.default=b;return a.default}); //# sourceMappingURL=selectors.min.js.map diff --git a/reportbuilder/amd/build/local/selectors.min.js.map b/reportbuilder/amd/build/local/selectors.min.js.map index 4714a85ae46..d8ac19dc5e4 100644 --- a/reportbuilder/amd/build/local/selectors.min.js.map +++ b/reportbuilder/amd/build/local/selectors.min.js.map @@ -1 +1 @@ -{"version":3,"sources":["../../src/local/selectors.js"],"names":["SELECTORS","regions","report","reportTable","columnHeader","filterButtonLabel","filtersForm","sidebarMenu","sidebarCard","settingsConditions","activeConditions","activeCondition","actions","reportActionPopup","reportCreate","reportEdit","reportDelete","reportAddColumn","reportRemoveColumn","reportAddCondition","reportRemoveCondition","sidebarSearch","forReport","reportId"],"mappings":"oJA+BA,GAAMA,CAAAA,CAAS,CAAG,CACdC,OAAO,CAAE,CACLC,MAAM,CAAE,6CADH,CAELC,WAAW,CAAE,uCAFR,CAGLC,YAAY,CAAE,iCAHT,CAILC,iBAAiB,CAAE,uCAJd,CAKLC,WAAW,CAAE,gCALR,CAMLC,WAAW,CAAE,gCANR,CAOLC,WAAW,CAAE,gCAPR,CAQLC,kBAAkB,CAAE,uCARf,CASLC,gBAAgB,CAAE,qCATb,CAULC,eAAe,CAAE,oCAVZ,CADK,CAadC,OAAO,CAAE,CACLC,iBAAiB,CAAE,uCADd,CAELC,YAAY,CAAE,iCAFT,CAGLC,UAAU,CAAE,+BAHP,CAILC,YAAY,CAAE,iCAJT,CAKLC,eAAe,CAAE,qCALZ,CAMLC,kBAAkB,CAAE,wCANf,CAOLC,kBAAkB,CAAE,wCAPf,CAQLC,qBAAqB,CAAE,2CARlB,CASLC,aAAa,CAAE,kCATV,CAbK,CAAlB,CAiCArB,CAAS,CAACsB,SAAV,CAAsB,SAAAC,CAAQ,kBAAOvB,CAAS,CAACC,OAAV,CAAkBC,MAAzB,8BAAmDqB,CAAnD,QAA9B,C,UAEevB,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 * Report builder selectors\n *\n * @module core_reportbuilder/local/selectors\n * @copyright 2021 Paul Holden \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * Selectors for the Report builder subsystem\n *\n * @property {Object} regions\n * @property {String} regions.systemReport System report page region\n * @property {String} regions.filterButtonLabel Filters form toggle region\n * @property {String} regions.filtersForm Filters form page region\n */\nconst SELECTORS = {\n regions: {\n report: '[data-region=\"core_reportbuilder/report\"]',\n reportTable: '[data-region=\"reportbuilder-table\"]',\n columnHeader: '[data-region=\"column-header\"]',\n filterButtonLabel: '[data-region=\"filter-button-label\"]',\n filtersForm: '[data-region=\"filters-form\"]',\n sidebarMenu: '[data-region=\"sidebar-menu\"]',\n sidebarCard: '[data-region=\"sidebar-card\"]',\n settingsConditions: '[data-region=\"settings-conditions\"]',\n activeConditions: '[data-region=\"active-conditions\"]',\n activeCondition: '[data-region=\"active-condition\"]',\n },\n actions: {\n reportActionPopup: '[data-action=\"report-action-popup\"]',\n reportCreate: '[data-action=\"report-create\"]',\n reportEdit: '[data-action=\"report-edit\"]',\n reportDelete: '[data-action=\"report-delete\"]',\n reportAddColumn: '[data-action=\"report-add-column\"]',\n reportRemoveColumn: '[data-action=\"report-remove-column\"]',\n reportAddCondition: '[data-action=\"report-add-condition\"]',\n reportRemoveCondition: '[data-action=\"report-remove-condition\"]',\n sidebarSearch: '[data-action=\"sidebar-search\"]',\n },\n};\n\n/**\n * Selector for given report\n *\n * @method forReport\n * @param {Number} reportId\n * @return {String}\n */\nSELECTORS.forReport = reportId => `${SELECTORS.regions.report}[data-report-id=\"${reportId}\"]`;\n\nexport default SELECTORS;\n"],"file":"selectors.min.js"} \ No newline at end of file +{"version":3,"sources":["../../src/local/selectors.js"],"names":["SELECTORS","regions","report","reportTable","columnHeader","filterButtonLabel","filtersForm","sidebarMenu","sidebarCard","settingsConditions","activeConditions","activeCondition","settingsFilters","activeFilters","activeFilter","actions","reportActionPopup","reportCreate","reportEdit","reportDelete","reportAddColumn","reportRemoveColumn","reportAddCondition","reportRemoveCondition","reportAddFilter","reportRemoveFilter","sidebarSearch","forReport","reportId"],"mappings":"oJA+BA,GAAMA,CAAAA,CAAS,CAAG,CACdC,OAAO,CAAE,CACLC,MAAM,CAAE,6CADH,CAELC,WAAW,CAAE,uCAFR,CAGLC,YAAY,CAAE,iCAHT,CAILC,iBAAiB,CAAE,uCAJd,CAKLC,WAAW,CAAE,gCALR,CAMLC,WAAW,CAAE,gCANR,CAOLC,WAAW,CAAE,gCAPR,CAQLC,kBAAkB,CAAE,uCARf,CASLC,gBAAgB,CAAE,qCATb,CAULC,eAAe,CAAE,oCAVZ,CAWLC,eAAe,CAAE,oCAXZ,CAYLC,aAAa,CAAE,kCAZV,CAaLC,YAAY,CAAE,iCAbT,CADK,CAgBdC,OAAO,CAAE,CACLC,iBAAiB,CAAE,uCADd,CAELC,YAAY,CAAE,iCAFT,CAGLC,UAAU,CAAE,+BAHP,CAILC,YAAY,CAAE,iCAJT,CAKLC,eAAe,CAAE,qCALZ,CAMLC,kBAAkB,CAAE,wCANf,CAOLC,kBAAkB,CAAE,wCAPf,CAQLC,qBAAqB,CAAE,2CARlB,CASLC,eAAe,CAAE,qCATZ,CAULC,kBAAkB,CAAE,wCAVf,CAWLC,aAAa,CAAE,kCAXV,CAhBK,CAAlB,CAsCA1B,CAAS,CAAC2B,SAAV,CAAsB,SAAAC,CAAQ,kBAAO5B,CAAS,CAACC,OAAV,CAAkBC,MAAzB,8BAAmD0B,CAAnD,QAA9B,C,UAEe5B,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 * Report builder selectors\n *\n * @module core_reportbuilder/local/selectors\n * @copyright 2021 Paul Holden \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * Selectors for the Report builder subsystem\n *\n * @property {Object} regions\n * @property {String} regions.systemReport System report page region\n * @property {String} regions.filterButtonLabel Filters form toggle region\n * @property {String} regions.filtersForm Filters form page region\n */\nconst SELECTORS = {\n regions: {\n report: '[data-region=\"core_reportbuilder/report\"]',\n reportTable: '[data-region=\"reportbuilder-table\"]',\n columnHeader: '[data-region=\"column-header\"]',\n filterButtonLabel: '[data-region=\"filter-button-label\"]',\n filtersForm: '[data-region=\"filters-form\"]',\n sidebarMenu: '[data-region=\"sidebar-menu\"]',\n sidebarCard: '[data-region=\"sidebar-card\"]',\n settingsConditions: '[data-region=\"settings-conditions\"]',\n activeConditions: '[data-region=\"active-conditions\"]',\n activeCondition: '[data-region=\"active-condition\"]',\n settingsFilters: '[data-region=\"settings-filters\"]',\n activeFilters: '[data-region=\"active-filters\"]',\n activeFilter: '[data-region=\"active-filter\"]',\n },\n actions: {\n reportActionPopup: '[data-action=\"report-action-popup\"]',\n reportCreate: '[data-action=\"report-create\"]',\n reportEdit: '[data-action=\"report-edit\"]',\n reportDelete: '[data-action=\"report-delete\"]',\n reportAddColumn: '[data-action=\"report-add-column\"]',\n reportRemoveColumn: '[data-action=\"report-remove-column\"]',\n reportAddCondition: '[data-action=\"report-add-condition\"]',\n reportRemoveCondition: '[data-action=\"report-remove-condition\"]',\n reportAddFilter: '[data-action=\"report-add-filter\"]',\n reportRemoveFilter: '[data-action=\"report-remove-filter\"]',\n sidebarSearch: '[data-action=\"sidebar-search\"]',\n },\n};\n\n/**\n * Selector for given report\n *\n * @method forReport\n * @param {Number} reportId\n * @return {String}\n */\nSELECTORS.forReport = reportId => `${SELECTORS.regions.report}[data-report-id=\"${reportId}\"]`;\n\nexport default SELECTORS;\n"],"file":"selectors.min.js"} \ No newline at end of file diff --git a/reportbuilder/amd/src/editor.js b/reportbuilder/amd/src/editor.js index 0340704c063..f3077c88097 100644 --- a/reportbuilder/amd/src/editor.js +++ b/reportbuilder/amd/src/editor.js @@ -31,6 +31,7 @@ import Templates from 'core/templates'; import * as reportSelectors from 'core_reportbuilder/local/selectors'; import {init as columnsEditorInit} from 'core_reportbuilder/local/editor/columns'; import {init as conditionsEditorInit} from 'core_reportbuilder/local/editor/conditions'; +import {init as filtersEditorInit} from 'core_reportbuilder/local/editor/filters'; import {getReport} from 'core_reportbuilder/local/repository/reports'; let initialized = false; @@ -43,6 +44,7 @@ export const init = () => { columnsEditorInit(reportElement, initialized); conditionsEditorInit(reportElement, initialized); + filtersEditorInit(reportElement, initialized); // Ensure we only add our listeners once (can be called multiple times by mustache template). if (initialized) { diff --git a/reportbuilder/amd/src/local/editor/filters.js b/reportbuilder/amd/src/local/editor/filters.js new file mode 100644 index 00000000000..eb84b81c009 --- /dev/null +++ b/reportbuilder/amd/src/local/editor/filters.js @@ -0,0 +1,147 @@ +// 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 . + +/** + * Report builder filters editor + * + * @module core_reportbuilder/local/editor/filters + * @package core_reportbuilder + * @copyright 2021 David Matamoros + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +"use strict"; + +import $ from 'jquery'; +import {dispatchEvent} from 'core/event_dispatcher'; +import 'core/inplace_editable'; +import Notification from 'core/notification'; +import Pending from 'core/pending'; +import SortableList from 'core/sortable_list'; +import {get_string as getString, get_strings as getStrings} from 'core/str'; +import Templates from 'core/templates'; +import {add as addToast} from 'core/toast'; +import * as reportEvents from 'core_reportbuilder/local/events'; +import * as reportSelectors from 'core_reportbuilder/local/selectors'; +import {addFilter, deleteFilter, reorderFilter} from 'core_reportbuilder/local/repository/filters'; + +/** + * Reload filters settings region + * + * @param {Element} reportElement + * @param {Object} templateContext + * @return {Promise} + */ +const reloadSettingsFiltersRegion = (reportElement, templateContext) => { + const pendingPromise = new Pending('core_reportbuilder/filters:reload'); + const settingsFiltersRegion = reportElement.querySelector(reportSelectors.regions.settingsFilters); + + return Templates.renderForPromise('core_reportbuilder/local/settings/filters', {filters: templateContext}) + .then(({html, js}) => { + Templates.replaceNode(settingsFiltersRegion, html, js); + return pendingPromise.resolve(); + }); +}; + +/** + * Initialise module + * + * @param {Element} reportElement + * @param {Boolean} initialized Ensure we only add our listeners once + */ +export const init = (reportElement, initialized) => { + if (initialized) { + return; + } + + reportElement.addEventListener('click', event => { + + // Add filter to report. + const reportAddFilter = event.target.closest(reportSelectors.actions.reportAddFilter); + if (reportAddFilter) { + event.preventDefault(); + + // Check if dropdown is closed with no filter selected. + if (reportAddFilter.value === '0') { + return; + } + + const pendingPromise = new Pending('core_reportbuilder/filters:add'); + + addFilter(reportElement.dataset.reportId, reportAddFilter.value) + .then(data => reloadSettingsFiltersRegion(reportElement, data)) + .then(() => getString('filteradded', 'core_reportbuilder', + reportAddFilter.options[reportAddFilter.selectedIndex].text)) + .then(addToast) + .then(() => pendingPromise.resolve()) + .catch(Notification.exception); + } + + // Remove filter from report. + const reportRemoveFilter = event.target.closest(reportSelectors.actions.reportRemoveFilter); + if (reportRemoveFilter) { + event.preventDefault(); + + const filterContainer = reportRemoveFilter.closest(reportSelectors.regions.activeFilter); + const filterName = filterContainer.dataset.filterName; + + getStrings([ + {key: 'deletefilter', component: 'core_reportbuilder', param: filterName}, + {key: 'deletefilterconfirm', component: 'core_reportbuilder', param: filterName}, + {key: 'delete', component: 'moodle'}, + ]).then(([confirmTitle, confirmText, confirmButton]) => { + Notification.confirm(confirmTitle, confirmText, confirmButton, null, () => { + const pendingPromise = new Pending('core_reportbuilder/filters:remove'); + + deleteFilter(reportElement.dataset.reportId, filterContainer.dataset.filterId) + .then(data => reloadSettingsFiltersRegion(reportElement, data)) + .then(() => getString('filterdeleted', 'core_reportbuilder', filterName)) + .then(addToast) + .then(() => { + dispatchEvent(reportEvents.tableReload, {}, reportElement); + return pendingPromise.resolve(); + }) + .catch(Notification.exception); + }); + return; + }).catch(Notification.exception); + } + }); + + // Initialize sortable list to handle active filters moving (note JQuery dependency, see MDL-72293 for resolution). + var activeFiltersSortableList = new SortableList(`${reportSelectors.regions.activeFilters} ul`, {isHorizontal: false}); + activeFiltersSortableList.getElementName = element => Promise.resolve(element.data('filterName')); + + $(reportElement).on(SortableList.EVENTS.DROP, 'li[data-filter-id]', (event, info) => { + if (info.positionChanged) { + const pendingPromise = new Pending('core_reportbuilder/filters:reorder'); + const filterId = info.element.data('filterId'); + const filterPosition = info.element.data('filterPosition'); + + // Select target position, if moving to the end then count number of element siblings. + let targetFilterPosition = info.targetNextElement.data('filterPosition') || info.element.siblings().length + 2; + if (targetFilterPosition > filterPosition) { + targetFilterPosition--; + } + + reorderFilter(reportElement.dataset.reportId, filterId, targetFilterPosition) + .then(data => reloadSettingsFiltersRegion(reportElement, data)) + .then(() => getString('filtermoved', 'core_reportbuilder', info.element.data('filterName'))) + .then(addToast) + .then(() => pendingPromise.resolve()) + .catch(Notification.exception); + } + }); +}; diff --git a/reportbuilder/amd/src/local/repository/filters.js b/reportbuilder/amd/src/local/repository/filters.js index 02c580fda71..4af4d12b840 100644 --- a/reportbuilder/amd/src/local/repository/filters.js +++ b/reportbuilder/amd/src/local/repository/filters.js @@ -38,3 +38,52 @@ export const resetFilters = reportId => { return Ajax.call([request])[0]; }; + +/** + * Add a filter to the given report + * + * @param {Number} reportId + * @param {String} uniqueIdentifier + * @return {Promise} + */ +export const addFilter = (reportId, uniqueIdentifier) => { + const request = { + methodname: 'core_reportbuilder_filters_add', + args: {reportid: reportId, uniqueidentifier: uniqueIdentifier} + }; + + return Ajax.call([request])[0]; +}; + +/** + * Remove filter from given report + * + * @param {Number} reportId + * @param {Number} filterId + * @return {Promise} + */ +export const deleteFilter = (reportId, filterId) => { + const request = { + methodname: 'core_reportbuilder_filters_delete', + args: {reportid: reportId, filterid: filterId} + }; + + return Ajax.call([request])[0]; +}; + +/** + * Reorder a filter in a given report + * + * @param {Number} reportId + * @param {Number} filterId + * @param {Number} position + * @return {Promise} + */ +export const reorderFilter = (reportId, filterId, position) => { + const request = { + methodname: 'core_reportbuilder_filters_reorder', + args: {reportid: reportId, filterid: filterId, position: position} + }; + + return Ajax.call([request])[0]; +}; diff --git a/reportbuilder/amd/src/local/selectors.js b/reportbuilder/amd/src/local/selectors.js index f982b74c638..583c5ed2880 100644 --- a/reportbuilder/amd/src/local/selectors.js +++ b/reportbuilder/amd/src/local/selectors.js @@ -41,6 +41,9 @@ const SELECTORS = { settingsConditions: '[data-region="settings-conditions"]', activeConditions: '[data-region="active-conditions"]', activeCondition: '[data-region="active-condition"]', + settingsFilters: '[data-region="settings-filters"]', + activeFilters: '[data-region="active-filters"]', + activeFilter: '[data-region="active-filter"]', }, actions: { reportActionPopup: '[data-action="report-action-popup"]', @@ -51,6 +54,8 @@ const SELECTORS = { reportRemoveColumn: '[data-action="report-remove-column"]', reportAddCondition: '[data-action="report-add-condition"]', reportRemoveCondition: '[data-action="report-remove-condition"]', + reportAddFilter: '[data-action="report-add-filter"]', + reportRemoveFilter: '[data-action="report-remove-filter"]', sidebarSearch: '[data-action="sidebar-search"]', }, }; diff --git a/reportbuilder/classes/datasource.php b/reportbuilder/classes/datasource.php index a4e6f2d4431..9a2f575a17e 100644 --- a/reportbuilder/classes/datasource.php +++ b/reportbuilder/classes/datasource.php @@ -95,6 +95,80 @@ abstract class datasource extends base { */ abstract public function get_default_columns(): array; + /** + * Add filters from the given entity name to be available to use in a custom report + * + * @param string $entityname + * @param array $include Include only these filters, if omitted then include all + * @param array $exclude Exclude these filters, if omitted then exclude none + * @throws coding_exception If both $include and $exclude are non-empty + */ + final protected function add_filters_from_entity(string $entityname, array $include = [], array $exclude = []): void { + if (!empty($include) && !empty($exclude)) { + throw new coding_exception('Cannot specify filters to include and exclude simultaneously'); + } + + $entity = $this->get_entity($entityname); + + // Retrieve filtered filters from entity, respecting given $include/$exclude parameters. + $filters = array_filter($entity->get_filters(), static function(filter $filter) use ($include, $exclude): bool { + if (!empty($include)) { + return in_array($filter->get_name(), $include); + } + + if (!empty($exclude)) { + return !in_array($filter->get_name(), $exclude); + } + + return true; + }); + + foreach ($filters as $filter) { + $this->add_filter($filter); + } + } + + /** + * Add default datasource filters to the report + * + * This method is optional and can be called when the report is created to add the default filters defined in the + * selected datasource. + */ + public function add_default_filters(): void { + $reportid = $this->get_report_persistent()->get('id'); + $filteridentifiers = $this->get_default_filters(); + foreach ($filteridentifiers as $uniqueidentifier) { + report::add_report_filter($reportid, $uniqueidentifier); + } + } + + /** + * Return the filters that will be added to the report once is created + * + * @return string[] + */ + abstract public function get_default_filters(): array; + + /** + * Return all configured report filters + * + * @return filter[] + */ + public function get_active_filters(): array { + $filters = []; + + $activefilters = filter_model::get_filter_records($this->get_report_persistent()->get('id'), 'filterorder'); + foreach ($activefilters as $filter) { + $instance = $this->get_filter($filter->get('uniqueidentifier')); + if ($instance !== null && $instance->get_is_available()) { + $filters[$instance->get_unique_identifier()] = $instance + ->set_persistent($filter); + } + } + + return $filters; + } + /** * Add conditions from the given entity name to be available to use in a custom report * diff --git a/reportbuilder/classes/external/custom_report_exporter.php b/reportbuilder/classes/external/custom_report_exporter.php index 9a6e92c6e95..bf63d9806ac 100644 --- a/reportbuilder/classes/external/custom_report_exporter.php +++ b/reportbuilder/classes/external/custom_report_exporter.php @@ -89,6 +89,8 @@ class custom_report_exporter extends persistent_exporter { 'table' => ['type' => PARAM_RAW], 'sidebarmenucards' => ['type' => custom_report_menu_cards_exporter::read_properties_definition()], 'conditions' => ['type' => custom_report_conditions_exporter::read_properties_definition()], + 'filters' => ['type' => custom_report_filters_exporter::read_properties_definition()], + 'filtersapplied' => ['type' => PARAM_INT], 'filtersform' => [ 'type' => PARAM_RAW, 'optional' => true, @@ -119,6 +121,7 @@ class custom_report_exporter extends persistent_exporter { $report = manager::get_report_from_persistent($this->persistent); $conditionsexporter = new custom_report_conditions_exporter(null, ['report' => $report]); + $filtersexporter = new custom_report_filters_exporter(null, ['report' => $report]); if ($this->editmode) { $menucardexporter = new custom_report_menu_cards_exporter(null, [ 'menucards' => report_helper::get_available_columns($report->get_report_persistent()) @@ -130,6 +133,8 @@ class custom_report_exporter extends persistent_exporter { 'table' => $output->render($table), 'sidebarmenucards' => $menucards, 'conditions' => (array) $conditionsexporter->export($output), + 'filters' => (array) $filtersexporter->export($output), + 'filtersapplied' => $report->get_applied_filter_count(), 'filtersform' => $filtersform, 'editmode' => (int)$this->editmode, 'showeditbutton' => $this->showeditbutton, diff --git a/reportbuilder/classes/external/custom_report_filters_exporter.php b/reportbuilder/classes/external/custom_report_filters_exporter.php new file mode 100644 index 00000000000..f05219cb960 --- /dev/null +++ b/reportbuilder/classes/external/custom_report_filters_exporter.php @@ -0,0 +1,162 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\external; + +use renderer_base; +use core\external\exporter; +use core_reportbuilder\local\report\base; +use core_reportbuilder\local\models\filter; +use core_reportbuilder\output\filter_heading_editable; + +/** + * Custom report filters exporter class + * + * @package core_reportbuilder + * @copyright 2021 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class custom_report_filters_exporter extends exporter { + + /** + * Return a list of objects that are related to the exporter + * + * @return array + */ + protected static function define_related(): array { + return [ + 'report' => base::class, + ]; + } + + /** + * Return the list of additional properties for read structure and export + * + * @return array[] + */ + protected static function define_other_properties(): array { + return [ + 'hasavailablefilters' => [ + 'type' => PARAM_BOOL, + ], + 'availablefilters' => [ + 'type' => [ + 'optiongroup' => [ + 'type' => [ + 'text' => ['type' => PARAM_TEXT], + 'values' => [ + 'type' => [ + 'value' => ['type' => PARAM_TEXT], + 'visiblename' => ['type' => PARAM_TEXT], + ], + 'multiple' => true, + ], + ], + ], + ], + 'multiple' => true, + 'optional' => true + ], + 'hasactivefilters' => [ + 'type' => PARAM_BOOL, + ], + 'activefilters' => [ + 'type' => [ + 'id' => ['type' => PARAM_INT], + 'heading' => ['type' => PARAM_TEXT], + 'headingeditable' => ['type' => PARAM_RAW], + 'sortorder' => ['type' => PARAM_INT], + 'movetitle' => ['type' => PARAM_TEXT], + 'entityname' => ['type' => PARAM_TEXT], + ], + 'multiple' => true, + 'optional' => true + ], + 'helpicon' => [ + 'type' => PARAM_RAW, + ], + ]; + } + + /** + * Get the additional values to inject while exporting + * + * @param renderer_base $output + * @return array + */ + protected function get_other_values(renderer_base $output): array { + /** @var base $report */ + $report = $this->related['report']; + + // Current filter instances contained in the report. + $filterinstances = filter::get_filter_records($report->get_report_persistent()->get('id'), 'filterorder'); + $filteridentifiers = array_map(static function(filter $filter): string { + return $filter->get('uniqueidentifier'); + }, $filterinstances); + + $availablefilters = $activefilters = []; + + // Populate available filters. + foreach ($report->get_filters() as $filter) { + if (in_array($filter->get_unique_identifier(), $filteridentifiers)) { + continue; + } + + $entityname = $filter->get_entity_name(); + if (!array_key_exists($entityname, $availablefilters)) { + $availablefilters[$entityname] = [ + 'optiongroup' => [ + 'text' => $report->get_entity_title($entityname)->out(), + 'values' => [], + ], + ]; + } + + $availablefilters[$entityname]['optiongroup']['values'][] = [ + 'value' => $filter->get_unique_identifier(), + 'visiblename' => $filter->get_header(), + ]; + } + + // Populate active filters. + foreach ($filterinstances as $filter) { + $editable = new filter_heading_editable($filter->get('id')); + + $filterinstance = $report->get_filter($filter->get('uniqueidentifier')); + $entityname = $filterinstance->get_entity_name(); + $displayvalue = $filterinstance->get_header(); + + $activefilters[] = [ + 'id' => $filter->get('id'), + 'entityname' => $report->get_entity_title($entityname)->out(), + 'heading' => $displayvalue, + 'headingeditable' => $editable->render($output), + 'sortorder' => $filter->get('filterorder'), + 'movetitle' => get_string('movefilter', 'core_reportbuilder', $displayvalue), + ]; + } + + return [ + 'hasavailablefilters' => !empty($availablefilters), + 'availablefilters' => array_values($availablefilters), + 'hasactivefilters' => !empty($activefilters), + 'activefilters' => $activefilters, + 'helpicon' => $output->help_icon('filters', 'core_reportbuilder'), + ]; + } +} diff --git a/reportbuilder/classes/external/filters/add.php b/reportbuilder/classes/external/filters/add.php new file mode 100644 index 00000000000..2977ec93d65 --- /dev/null +++ b/reportbuilder/classes/external/filters/add.php @@ -0,0 +1,96 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\external\filters; + +use external_api; +use external_function_parameters; +use external_single_structure; +use external_value; +use core_reportbuilder\manager; +use core_reportbuilder\permission; +use core_reportbuilder\external\custom_report_filters_exporter; +use core_reportbuilder\local\helpers\report; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once("{$CFG->libdir}/externallib.php"); + +/** + * External method for adding report filters + * + * @package core_reportbuilder + * @copyright 2021 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class add extends external_api { + + /** + * External method parameters + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters([ + 'reportid' => new external_value(PARAM_INT, 'Report ID'), + 'uniqueidentifier' => new external_value(PARAM_RAW, 'Unique identifier of the filter'), + ]); + } + + /** + * External method execution + * + * @param int $reportid + * @param string $uniqueidentifier + * @return array + */ + public static function execute(int $reportid, string $uniqueidentifier): array { + global $PAGE; + + [ + 'reportid' => $reportid, + 'uniqueidentifier' => $uniqueidentifier, + ] = self::validate_parameters(self::execute_parameters(), [ + 'reportid' => $reportid, + 'uniqueidentifier' => $uniqueidentifier, + ]); + + $report = manager::get_report_from_id($reportid); + + self::validate_context($report->get_context()); + permission::require_can_edit_report($report->get_report_persistent()); + + report::add_report_filter($reportid, $uniqueidentifier); + + $exporter = new custom_report_filters_exporter(null, [ + 'report' => $report, + ]); + + return (array) $exporter->export($PAGE->get_renderer('core')); + } + + /** + * External method return value + * + * @return external_single_structure + */ + public static function execute_returns(): external_single_structure { + return custom_report_filters_exporter::get_read_structure(); + } +} diff --git a/reportbuilder/classes/external/filters/delete.php b/reportbuilder/classes/external/filters/delete.php new file mode 100644 index 00000000000..ee57495c0a4 --- /dev/null +++ b/reportbuilder/classes/external/filters/delete.php @@ -0,0 +1,96 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\external\filters; + +use external_api; +use external_function_parameters; +use external_single_structure; +use external_value; +use core_reportbuilder\manager; +use core_reportbuilder\permission; +use core_reportbuilder\external\custom_report_filters_exporter; +use core_reportbuilder\local\helpers\report; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once("{$CFG->libdir}/externallib.php"); + +/** + * External method for deleting report filters + * + * @package core_reportbuilder + * @copyright 2021 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class delete extends external_api { + + /** + * External method parameters + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters([ + 'reportid' => new external_value(PARAM_INT, 'Report ID'), + 'filterid' => new external_value(PARAM_INT, 'Filter ID'), + ]); + } + + /** + * External method execution + * + * @param int $reportid + * @param int $filterid + * @return array + */ + public static function execute(int $reportid, int $filterid): array { + global $PAGE; + + [ + 'reportid' => $reportid, + 'filterid' => $filterid, + ] = self::validate_parameters(self::execute_parameters(), [ + 'reportid' => $reportid, + 'filterid' => $filterid, + ]); + + $report = manager::get_report_from_id($reportid); + + self::validate_context($report->get_context()); + permission::require_can_edit_report($report->get_report_persistent()); + + report::delete_report_filter($reportid, $filterid); + + $exporter = new custom_report_filters_exporter(null, [ + 'report' => $report, + ]); + + return (array) $exporter->export($PAGE->get_renderer('core')); + } + + /** + * External method return value + * + * @return external_single_structure + */ + public static function execute_returns(): external_single_structure { + return custom_report_filters_exporter::get_read_structure(); + } +} diff --git a/reportbuilder/classes/external/filters/reorder.php b/reportbuilder/classes/external/filters/reorder.php new file mode 100644 index 00000000000..55cee5e5817 --- /dev/null +++ b/reportbuilder/classes/external/filters/reorder.php @@ -0,0 +1,100 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\external\filters; + +use external_api; +use external_function_parameters; +use external_single_structure; +use external_value; +use core_reportbuilder\manager; +use core_reportbuilder\permission; +use core_reportbuilder\external\custom_report_filters_exporter; +use core_reportbuilder\local\helpers\report; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once("{$CFG->libdir}/externallib.php"); + +/** + * External method for re-ordering report filters + * + * @package core_reportbuilder + * @copyright 2021 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class reorder extends external_api { + + /** + * External method parameters + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters([ + 'reportid' => new external_value(PARAM_INT, 'Report ID'), + 'filterid' => new external_value(PARAM_INT, 'Filter ID'), + 'position' => new external_value(PARAM_INT, 'New filter position') + ]); + } + + /** + * External method execution + * + * @param int $reportid + * @param int $filterid + * @param int $position + * @return array + */ + public static function execute(int $reportid, int $filterid, int $position): array { + global $PAGE; + + [ + 'reportid' => $reportid, + 'filterid' => $filterid, + 'position' => $position, + ] = self::validate_parameters(self::execute_parameters(), [ + 'reportid' => $reportid, + 'filterid' => $filterid, + 'position' => $position, + ]); + + $report = manager::get_report_from_id($reportid); + + self::validate_context($report->get_context()); + permission::require_can_edit_report($report->get_report_persistent()); + + report::reorder_report_filter($reportid, $filterid, $position); + + $exporter = new custom_report_filters_exporter(null, [ + 'report' => $report, + ]); + + return (array) $exporter->export($PAGE->get_renderer('core')); + } + + /** + * External method return value + * + * @return external_single_structure + */ + public static function execute_returns(): external_single_structure { + return custom_report_filters_exporter::get_read_structure(); + } +} diff --git a/reportbuilder/classes/local/helpers/report.php b/reportbuilder/classes/local/helpers/report.php index 7ee11551f2d..398cd9c03ed 100644 --- a/reportbuilder/classes/local/helpers/report.php +++ b/reportbuilder/classes/local/helpers/report.php @@ -256,6 +256,86 @@ class report { return static::reorder_persistents_by_field($condition, $conditions, $position, 'filterorder'); } + /** + * Add given filter to report + * + * @param int $reportid + * @param string $uniqueidentifier + * @return filter + * @throws invalid_parameter_exception + */ + public static function add_report_filter(int $reportid, string $uniqueidentifier): filter { + $report = manager::get_report_from_id($reportid); + + $reportfilters = $report->get_filters(); + if (!array_key_exists($uniqueidentifier, $reportfilters)) { + throw new invalid_parameter_exception('Invalid filter'); + } + + $filter = new filter(0, (object) [ + 'reportid' => $reportid, + 'uniqueidentifier' => $uniqueidentifier, + 'filterorder' => filter::get_max_filterorder($reportid) + 1, + ]); + + return $filter->create(); + } + + /** + * Delete given filter from report + * + * @param int $reportid + * @param int $filterid + * @return bool + * @throws invalid_parameter_exception + */ + public static function delete_report_filter(int $reportid, int $filterid): bool { + global $DB; + + $filter = filter::get_filter_record($reportid, $filterid); + if ($filter === false) { + throw new invalid_parameter_exception('Invalid filter'); + } + + // After deletion, re-index remaining report filters. + if ($result = $filter->delete()) { + $sqlupdateorder = ' + UPDATE {' . filter::TABLE . '} + SET filterorder = filterorder - 1 + WHERE reportid = :reportid + AND filterorder > :filterorder + AND iscondition = 0'; + + $DB->execute($sqlupdateorder, ['reportid' => $reportid, 'filterorder' => $filter->get('filterorder')]); + } + + return $result; + } + + /** + * Re-order given filter within a report + * + * @param int $reportid + * @param int $filterid + * @param int $position + * @return bool + * @throws invalid_parameter_exception + */ + public static function reorder_report_filter(int $reportid, int $filterid, int $position): bool { + $filter = filter::get_filter_record($reportid, $filterid); + if ($filter === false) { + throw new invalid_parameter_exception('Invalid filter'); + } + + // Get the rest of the report filters, excluding the one we are moving. + $filters = filter::get_records_select('reportid = :reportid AND iscondition = 0 AND id <> :id', [ + 'reportid' => $reportid, + 'id' => $filterid, + ], 'filterorder'); + + return static::reorder_persistents_by_field($filter, $filters, $position, 'filterorder'); + } + /** * Get available columns for a given report * diff --git a/reportbuilder/classes/local/report/base.php b/reportbuilder/classes/local/report/base.php index 725edb3e61d..1bc150219c1 100644 --- a/reportbuilder/classes/local/report/base.php +++ b/reportbuilder/classes/local/report/base.php @@ -287,6 +287,7 @@ abstract class base { /** * Returns title of given report entity * + * @param string $name * @return lang_string * @throws coding_exception */ @@ -345,17 +346,10 @@ abstract class base { * Each entity must have already been added to the report before calling this method * * @param string[] $columns Unique identifier of each entity column - * @throws coding_exception For unknown entities */ final protected function add_columns_from_entities(array $columns): void { foreach ($columns as $column) { - [$entityname, $columnname] = explode(':', $column, 2); - - if (!array_key_exists($entityname, $this->entities)) { - throw new coding_exception('Invalid entity name', $entityname); - } - - $this->add_column($this->entities[$entityname]->get_column($columnname)); + $this->add_column_from_entity($column); } } @@ -502,7 +496,6 @@ abstract class base { return (array) json_decode($conditions); } - /** * Adds a filter to the report * @@ -530,23 +523,30 @@ abstract class base { return $filter; } + /** + * Add given filter to the report from an entity + * + * The entity must have already been added to the report before calling this method + * + * @param string $uniqueidentifier + * @return filter + */ + final protected function add_filter_from_entity(string $uniqueidentifier): filter { + [$entityname, $filtername] = explode(':', $uniqueidentifier, 2); + + return $this->add_filter($this->get_entity($entityname)->get_filter($filtername)); + } + /** * Add given filters to the report from one or more entities * * Each entity must have already been added to the report before calling this method * * @param string[] $filters Unique identifier of each entity filter - * @throws coding_exception For unknown entities */ final protected function add_filters_from_entities(array $filters): void { foreach ($filters as $filter) { - [$entityname, $filtername] = explode(':', $filter, 2); - - if (!array_key_exists($entityname, $this->entities)) { - throw new coding_exception('Invalid entity name', $entityname); - } - - $this->add_filter($this->entities[$entityname]->get_filter($filtername)); + $this->add_filter_from_entity($filter); } } @@ -581,7 +581,7 @@ abstract class base { } /** - * Return all report filter instances + * Return all active report filter instances * * @return filter_base[] */ diff --git a/reportbuilder/classes/local/report/filter.php b/reportbuilder/classes/local/report/filter.php index aa7757b96af..b6d884354b3 100644 --- a/reportbuilder/classes/local/report/filter.php +++ b/reportbuilder/classes/local/report/filter.php @@ -21,6 +21,7 @@ namespace core_reportbuilder\local\report; use lang_string; use moodle_exception; use core_reportbuilder\local\filters\base; +use core_reportbuilder\local\models\filter as filter_model; /** * Class to represent a report filter @@ -61,6 +62,9 @@ final class filter { /** @var array $limitoperators */ protected $limitoperators = []; + /** @var filter_model $persistent */ + protected $persistent; + /** * Filter constructor * @@ -307,4 +311,24 @@ final class filter { return array_intersect_key($operators, array_flip($this->limitoperators)); } + + /** + * Set filter persistent + * + * @param filter_model $persistent + * @return self + */ + public function set_persistent(filter_model $persistent): self { + $this->persistent = $persistent; + return $this; + } + + /** + * Return filter persistent + * + * @return filter_model|null + */ + public function get_persistent(): ?filter_model { + return $this->persistent ?? null; + } } diff --git a/reportbuilder/classes/output/filter_heading_editable.php b/reportbuilder/classes/output/filter_heading_editable.php new file mode 100644 index 00000000000..2353a43ddfc --- /dev/null +++ b/reportbuilder/classes/output/filter_heading_editable.php @@ -0,0 +1,81 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\output; + +use core_external; +use core_reportbuilder\local\models\filter; +use core\output\inplace_editable; +use core_reportbuilder\manager; +use core_reportbuilder\permission; +use core_reportbuilder\local\models\report; + +/** + * Filter heading editable component + * + * @package core_reportbuilder + * @copyright 2021 David Matamoros + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class filter_heading_editable extends inplace_editable { + + /** + * Class constructor + * + * @param int $filterid + * @param filter|null $filter + */ + public function __construct(int $filterid, ?filter $filter = null) { + if ($filter === null) { + $filter = new filter($filterid); + } + + $report = $filter->get_report(); + $editable = permission::can_edit_report($report); + + $filterinstance = manager::get_report_from_persistent($report) + ->get_filter($filter->get('uniqueidentifier')); + + $displayvalue = $filter->get('heading') ?: $filterinstance->get_header(); + + parent::__construct('core_reportbuilder', 'filterheading', $filter->get('id'), $editable, $displayvalue, + $displayvalue, get_string('renamefilter', 'core_reportbuilder', $filterinstance->get_header())); + } + + /** + * Update filter persistent and return self, called from inplace_editable callback + * + * @param int $filterid + * @param string $value + * @return self + */ + public static function update(int $filterid, string $value): self { + $filter = new filter($filterid); + + $report = $filter->get_report(); + core_external::validate_context($report->get_context()); + permission::require_can_edit_report($report); + + $value = clean_param($value, PARAM_TEXT); + $filter + ->set('heading', $value) + ->update(); + + return new self(0, $filter); + } +} diff --git a/reportbuilder/classes/table/base_report_table.php b/reportbuilder/classes/table/base_report_table.php index 11d2c2d21e9..981d9c6f1b6 100644 --- a/reportbuilder/classes/table/base_report_table.php +++ b/reportbuilder/classes/table/base_report_table.php @@ -28,6 +28,7 @@ use core_reportbuilder\local\filters\base; use core_reportbuilder\local\models\report; use core_reportbuilder\local\report\base as base_report; use core_reportbuilder\local\report\filter; +use core\output\notification; defined('MOODLE_INTERNAL') || die; @@ -51,6 +52,9 @@ abstract class base_report_table extends table_sql implements dynamic, renderabl /** @var string $groupbysql */ protected $groupbysql = ''; + /** @var bool $applyfilters */ + protected $applyfilters = true; + /** * Initialises table SQL properties * @@ -81,19 +85,23 @@ abstract class base_report_table extends table_sql implements dynamic, renderabl } // For each filter, we also need to apply their values (will differ according to user viewing the report). - $filtervalues = $this->report->get_filter_values(); - foreach ($this->report->get_active_filters() as $filter) { - [$filtersql, $filterparams] = $this->get_filter_sql($filter, $filtervalues); - if ($filtersql !== '') { - $joins = array_merge($joins, $filter->get_joins()); - $wheres[] = "({$filtersql})"; - $params = array_merge($params, $filterparams); + if ($this->applyfilters) { + $filtervalues = $this->report->get_filter_values(); + foreach ($this->report->get_active_filters() as $filter) { + [$filtersql, $filterparams] = $this->get_filter_sql($filter, $filtervalues); + if ($filtersql !== '') { + $joins = array_merge($joins, $filter->get_joins()); + $wheres[] = "({$filtersql})"; + $params = array_merge($params, $filterparams); + } } } - $wheresql = '1=1'; + // Join all the filters into a SQL WHERE clause, falling back to all records. if (!empty($wheres)) { $wheresql = implode(' AND ', $wheres); + } else { + $wheresql = '1=1'; } if (!empty($groupby)) { @@ -115,6 +123,16 @@ abstract class base_report_table extends table_sql implements dynamic, renderabl ) {$counttablealias}", $params); } + /** + * Whether active filters should be applied to the report, defaults to true except in the case where we are editing a report + * and we do not want filters to be applied to it + * + * @param bool $applyfilters + */ + public function set_filters_applied(bool $applyfilters): void { + $this->applyfilters = $applyfilters; + } + /** * Return SQL fragments from given filter instance suitable for inclusion in table SQL * @@ -169,4 +187,18 @@ abstract class base_report_table extends table_sql implements dynamic, renderabl public function guess_base_url(): void { $this->baseurl = new moodle_url('/'); } + + /** + * Override print_nothing_to_display to modity the output styles. + */ + public function print_nothing_to_display() { + global $OUTPUT; + + echo $this->get_dynamic_table_html_start(); + echo $this->render_reset_button(); + + echo $OUTPUT->render(new notification(get_string('nothingtodisplay'), notification::NOTIFY_INFO)); + + echo $this->get_dynamic_table_html_end(); + } } diff --git a/reportbuilder/classes/table/custom_report_table.php b/reportbuilder/classes/table/custom_report_table.php index 00a1bb7ba0e..1a9b78c0598 100644 --- a/reportbuilder/classes/table/custom_report_table.php +++ b/reportbuilder/classes/table/custom_report_table.php @@ -18,6 +18,7 @@ declare(strict_types=1); namespace core_reportbuilder\table; +use core\output\notification; use html_writer; use moodle_exception; use moodle_url; @@ -41,6 +42,9 @@ class custom_report_table extends base_report_table { /** @var string Unique ID prefix for the table */ private const UNIQUEID_PREFIX = 'custom-report-table-'; + /** @var bool Whether filters should be applied in report (we don't want them when editing) */ + protected const REPORT_APPLY_FILTERS = false; + /** * Table constructor. Note that the passed unique ID value must match the pattern "custom-report-table-(\d+)" so that * dynamic updates continue to load the same report @@ -101,6 +105,8 @@ class custom_report_table extends base_report_table { $this->pageable(true); // Initialise table SQL properties. + $this->set_filters_applied(static::REPORT_APPLY_FILTERS); + $fieldsql = implode(', ', $fields); $this->init_sql($fieldsql, "{{$maintable}} {$maintablealias}", $joins, $where, $params, $groupby); } @@ -195,7 +201,6 @@ class custom_report_table extends base_report_table { $columns = $this->get_active_columns(); if (empty($columns)) { - $this->print_nothing_to_display(); return; } @@ -250,13 +255,21 @@ class custom_report_table extends base_report_table { } /** - * Override end of HTML to ensure that column headers are always added + * Override print_nothing_to_display to ensure that column headers are always added. */ - public function finish_html() { - if ($this->totalrows == 0 && !$this->started_output) { - $this->start_output(); - } + public function print_nothing_to_display() { + global $OUTPUT; - parent::finish_html(); + $this->start_html(); + $this->print_headers(); + echo html_writer::end_tag('table'); + echo html_writer::end_tag('div'); + $this->wrap_html_finish(); + + $notification = (new notification(get_string('nothingtodisplay'), notification::NOTIFY_INFO)) + ->set_extra_classes(['mt-3']); + echo $OUTPUT->render($notification); + + echo $this->get_dynamic_table_html_end(); } } diff --git a/reportbuilder/lib.php b/reportbuilder/lib.php index b868c5b48c1..a3ef881635d 100644 --- a/reportbuilder/lib.php +++ b/reportbuilder/lib.php @@ -61,6 +61,9 @@ function core_reportbuilder_inplace_editable($itemtype, $itemid, $newvalue) { case 'columnheading': return \core_reportbuilder\output\column_heading_editable::update($itemid, $newvalue); + case 'filterheading': + return \core_reportbuilder\output\filter_heading_editable::update($itemid, $newvalue); + default: return false; } diff --git a/reportbuilder/templates/local/settings/area.mustache b/reportbuilder/templates/local/settings/area.mustache index 58e8d2a1a72..287deb8375e 100644 --- a/reportbuilder/templates/local/settings/area.mustache +++ b/reportbuilder/templates/local/settings/area.mustache @@ -40,6 +40,16 @@ {{/body}} {{/ core_reportbuilder/toggle_card }} + {{< core_reportbuilder/toggle_card }} + {{$collapsed}}collapsed{{/collapsed}} + {{$id}}settingsfilters{{/id}} + {{$header}}{{#str}} filters, core_reportbuilder {{/str}}{{/header}} + {{$helpicon}}{{{filters.helpicon}}}{{/helpicon}} + {{$body}} + {{> core_reportbuilder/local/settings/filters}} + {{/body}} + {{/ core_reportbuilder/toggle_card }} + diff --git a/reportbuilder/templates/local/settings/filters.mustache b/reportbuilder/templates/local/settings/filters.mustache new file mode 100644 index 00000000000..585f6064d0f --- /dev/null +++ b/reportbuilder/templates/local/settings/filters.mustache @@ -0,0 +1,79 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_reportbuilder/local/settings/filters + + Template for custom report filters settings area + + Example context (json): + { + "id": 3, + "filtersform": "form" + } +}} + +
+ {{#filters}} + {{#hasavailablefilters}} +
+ + +
+ {{/hasavailablefilters}} + +
+ {{#hasactivefilters}} + + {{/hasactivefilters}} + + {{^hasactivefilters}} + {{< core_reportbuilder/local/settings/empty_message }} + {{$nothingtoshow}} {{#str}} nofilters, core_reportbuilder {{/str}} {{/nothingtoshow}} + {{/ core_reportbuilder/local/settings/empty_message }} + {{/hasactivefilters}} +
+ {{/filters}} +
\ No newline at end of file diff --git a/reportbuilder/tests/behat/filtereditor.feature b/reportbuilder/tests/behat/filtereditor.feature new file mode 100644 index 00000000000..9e4a6cf07fc --- /dev/null +++ b/reportbuilder/tests/behat/filtereditor.feature @@ -0,0 +1,152 @@ +@core_reportbuilder @javascript +Feature: Manage custom report filters + In order to manage the filters of custom reports + As an admin + I need to add, edit and delete filters in a report + + Scenario: Add filter to report + Given the following "core_reportbuilder > Reports" exist: + | name | source | default | + | My report | core_user\reportbuilder\datasource\users | 0 | + And I am on the "My report" "reportbuilder > Editor" page logged in as "admin" + When I click on "Show/hide settings sidebar" "button" + And I click on "Show/hide 'Filters'" "button" + Then I should see "There are no filters selected" in the "[data-region='active-filters']" "css_element" + And I set the field "Select a filter" to "Email address" + And I should see "Added filter 'Email address'" + And I should not see "There are no filters selected" in the "[data-region='active-filters']" "css_element" + And I should see "Email address" in the "[data-region='active-filters']" "css_element" + + Scenario: Rename filter in report + Given the following "core_reportbuilder > Reports" exist: + | name | source | default | + | My report | core_user\reportbuilder\datasource\users | 0 | + And the following "core_reportbuilder > Filters" exist: + | report | uniqueidentifier | + | My report | user:email | + And I am on the "My report" "reportbuilder > Editor" page logged in as "admin" + When I click on "Show/hide settings sidebar" "button" + And I click on "Show/hide 'Filters'" "button" + And I set the field "Rename filter 'Email address'" to "My Email filter" + And I reload the page + And I click on "Show/hide settings sidebar" "button" + And I click on "Show/hide 'Filters'" "button" + Then I should see "My Email filter" in the "[data-region='active-filters']" "css_element" + + Scenario: Move filter in report + Given the following "core_reportbuilder > Reports" exist: + | name | source | default | + | My report | core_user\reportbuilder\datasource\users | 0 | + And the following "core_reportbuilder > Filters" exist: + | report | uniqueidentifier | + | My report | user:fullname | + | My report | user:email | + | My report | user:country | + And I am on the "My report" "reportbuilder > Editor" page logged in as "admin" + When I click on "Show/hide settings sidebar" "button" + And I click on "Show/hide 'Filters'" "button" + And I click on "Move filter 'Country'" "button" + And I click on "After \"Full name\"" "link" in the "Move filter 'Country'" "dialogue" + Then I should see "Moved filter 'Country'" + And "Country" "text" should appear before "Email address" "text" + + Scenario: Delete filter from report + Given the following "core_reportbuilder > Reports" exist: + | name | source | default | + | My report | core_user\reportbuilder\datasource\users | 0 | + And the following "core_reportbuilder > Filters" exist: + | report | uniqueidentifier | + | My report | user:email | + And I am on the "My report" "reportbuilder > Editor" page logged in as "admin" + When I click on "Show/hide settings sidebar" "button" + And I click on "Show/hide 'Filters'" "button" + And I click on "Delete filter 'Email address'" "link" + And I click on "Delete" "button" in the "Delete filter 'Email address'" "dialogue" + Then I should see "Deleted filter 'Email address'" + And I should see "There are no filters selected" in the "[data-region='active-filters']" "css_element" + And I should not see "Email address" in the "[data-region='active-filters']" "css_element" + + Scenario: Use report filters when previewing report + Given the following "users" exist: + | username | firstname | lastname | + | user1 | User | 1 | + | user2 | User | 2 | + | user3 | User | 3 | + And the following "core_reportbuilder > Reports" exist: + | name | source | default | + | My report | core_user\reportbuilder\datasource\users | 0 | + And the following "core_reportbuilder > Columns" exist: + | report | uniqueidentifier | + | My report | user:fullname | + | My report | user:email | + And the following "core_reportbuilder > Filters" exist: + | report | uniqueidentifier | + | My report | user:fullname | + And I am on the "My report" "reportbuilder > Editor" page logged in as "admin" + And I change window size to "large" + And I should see "user1@example.com" in the ".reportbuilder-table" "css_element" + And I should see "user2@example.com" in the ".reportbuilder-table" "css_element" + And I should see "user3@example.com" in the ".reportbuilder-table" "css_element" + When I click on "Switch to preview mode" "button" + And I click on "Filters" "button" + And I set the following fields in the "Full name" "core_reportbuilder > Filter" to these values: + | Full name operator | Does not contain | + | Full name value | User 2 | + And I click on "Apply" "button" in the "[data-region='report-filters']" "css_element" + Then I should see "Filters applied" + And I should see "Filters (1)" in the "#dropdownFiltersButton" "css_element" + And the following should exist in the "reportbuilder-table" table: + | Full name | Email address | + | User 1 | user1@example.com | + | User 3 | user3@example.com | + And the following should not exist in the "reportbuilder-table" table: + | Full name | Email address | + | User 2 | user2@example.com | + + Scenario: Use report filters when previewing report that contains same condition + Given the following "users" exist: + | username | firstname | lastname | + | user1 | User | 1 | + | user2 | User | 2 | + | user3 | User | 3 | + And the following "core_reportbuilder > Reports" exist: + | name | source | default | + | My report | core_user\reportbuilder\datasource\users | 0 | + And the following "core_reportbuilder > Columns" exist: + | report | uniqueidentifier | + | My report | user:fullname | + | My report | user:email | + And the following "core_reportbuilder > Conditions" exist: + | report | uniqueidentifier | + | My report | user:email | + And the following "core_reportbuilder > Filters" exist: + | report | uniqueidentifier | + | My report | user:email | + And I am on the "My report" "reportbuilder > Editor" page logged in as "admin" + And I change window size to "large" + And I should see "user1@example.com" in the ".reportbuilder-table" "css_element" + And I should see "user2@example.com" in the ".reportbuilder-table" "css_element" + And I should see "user3@example.com" in the ".reportbuilder-table" "css_element" + # Set a condition to the report. + When I click on "Show/hide settings sidebar" "button" + And I click on "Show/hide 'Conditions'" "button" + And I set the following fields in the "Email address" "core_reportbuilder > Condition" to these values: + | Email address operator | Is not equal to | + | Email address value | user3@example.com | + And I click on "Apply" "button" in the "#settingsconditions" "css_element" + And I click on "Switch to preview mode" "button" + And I click on "Filters" "button" + And I set the following fields in the "Email address" "core_reportbuilder > Filter" to these values: + | Email address operator | Is not equal to | + | Email address value | user2@example.com | + And I click on "Apply" "button" in the "[data-region='report-filters']" "css_element" + Then I should see "Filters applied" + And I should see "Filters (1)" in the "#dropdownFiltersButton" "css_element" + # Assert we haven't overridden the condition and user3 is still not showing in the report. + And the following should exist in the "reportbuilder-table" table: + | Full name | Email address | + | User 1 | user1@example.com | + And the following should not exist in the "reportbuilder-table" table: + | Full name | Email address | + | User 2 | user2@example.com | + | User 3 | user3@example.com | diff --git a/reportbuilder/tests/external/filters/add_test.php b/reportbuilder/tests/external/filters/add_test.php new file mode 100644 index 00000000000..5daf87cd1d6 --- /dev/null +++ b/reportbuilder/tests/external/filters/add_test.php @@ -0,0 +1,93 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\external\filters; + +use core_reportbuilder\manager; +use core_reportbuilder_generator; +use external_api; +use externallib_advanced_testcase; +use core_reportbuilder\report_access_exception; +use core_reportbuilder\local\models\filter; +use core_user\reportbuilder\datasource\users; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once("{$CFG->dirroot}/webservice/tests/helpers.php"); + +/** + * Unit tests of external class for adding report filters + * + * @package core_reportbuilder + * @covers \core_reportbuilder\external\filters\add + * @copyright 2021 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class add_test extends externallib_advanced_testcase { + + /** + * Text execute method + */ + public function test_execute(): void { + $this->resetAfterTest(); + $this->setAdminUser(); + + /** @var core_reportbuilder_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); + + $report = $generator->create_report([ + 'name' => 'My report', + 'source' => users::class, + 'default' => false, + ]); + + // Add filter. + $result = add::execute($report->get('id'), 'user:fullname'); + $result = external_api::clean_returnvalue(add::execute_returns(), $result); + $this->assertTrue($result['hasavailablefilters']); + $this->assertEquals('User', $result['availablefilters'][0]['optiongroup']['text']); + $this->assertNotEmpty($result['availablefilters'][0]['optiongroup']['values']); + $this->assertTrue($result['hasactivefilters']); + $this->assertCount(1, $result['activefilters']); + $this->assertEquals('Full name', $result['activefilters'][0]['heading']); + + // Assert report filters. + $filters = filter::get_filter_records($report->get('id')); + $this->assertCount(1, $filters); + $this->assertEquals('user:fullname', reset($filters)->get('uniqueidentifier')); + } + + /** + * Test execute method for a user without permission to edit reports + */ + public function test_execute_access_exception(): void { + $this->resetAfterTest(); + + /** @var core_reportbuilder_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); + $report = $generator->create_report(['name' => 'My report', 'source' => users::class]); + + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + + $this->expectException(report_access_exception::class); + $this->expectExceptionMessage('You can not edit this report'); + add::execute($report->get('id'), 'user:fullname'); + } +} diff --git a/reportbuilder/tests/external/filters/delete_test.php b/reportbuilder/tests/external/filters/delete_test.php new file mode 100644 index 00000000000..aca9a7603a4 --- /dev/null +++ b/reportbuilder/tests/external/filters/delete_test.php @@ -0,0 +1,98 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\external\filters; + +use core_reportbuilder_generator; +use external_api; +use externallib_advanced_testcase; +use core_reportbuilder\report_access_exception; +use core_reportbuilder\local\models\filter; +use core_user\reportbuilder\datasource\users; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once("{$CFG->dirroot}/webservice/tests/helpers.php"); + +/** + * Unit tests of external class for deleting report filters + * + * @package core_reportbuilder + * @covers \core_reportbuilder\external\filters\delete + * @copyright 2021 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class delete_test extends externallib_advanced_testcase { + + /** + * Text execute method + */ + public function test_execute(): void { + $this->resetAfterTest(); + $this->setAdminUser(); + + /** @var core_reportbuilder_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); + + $report = $generator->create_report([ + 'name' => 'My report', + 'source' => users::class, + 'default' => false, + ]); + + // Add two filters. + $filterfullname = $generator->create_filter(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:fullname']); + $generator->create_filter(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:email']); + + // Delete the first filter. + $result = delete::execute($report->get('id'), $filterfullname->get('id')); + $result = external_api::clean_returnvalue(delete::execute_returns(), $result); + $this->assertTrue($result['hasavailablefilters']); + $this->assertEquals('User', $result['availablefilters'][0]['optiongroup']['text']); + $this->assertNotEmpty($result['availablefilters'][0]['optiongroup']['values']); + $this->assertTrue($result['hasactivefilters']); + $this->assertCount(1, $result['activefilters']); + $this->assertEquals('Email address', $result['activefilters'][0]['heading']); + + // Assert report filters. + $filters = filter::get_filter_records($report->get('id')); + $this->assertCount(1, $filters); + $this->assertEquals('user:email', reset($filters)->get('uniqueidentifier')); + } + + /** + * Test execute method for a user without permission to edit reports + */ + public function test_execute_access_exception(): void { + $this->resetAfterTest(); + + /** @var core_reportbuilder_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); + + $report = $generator->create_report(['name' => 'My report', 'source' => users::class]); + $filter = $generator->create_filter(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:email']); + + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + + $this->expectException(report_access_exception::class); + $this->expectExceptionMessage('You can not edit this report'); + delete::execute($report->get('id'), $filter->get('id')); + } +} diff --git a/reportbuilder/tests/external/filters/reorder_test.php b/reportbuilder/tests/external/filters/reorder_test.php new file mode 100644 index 00000000000..9b6c06d9957 --- /dev/null +++ b/reportbuilder/tests/external/filters/reorder_test.php @@ -0,0 +1,108 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\external\filters; + +use core_reportbuilder_generator; +use external_api; +use externallib_advanced_testcase; +use core_reportbuilder\report_access_exception; +use core_reportbuilder\local\models\filter; +use core_user\reportbuilder\datasource\users; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once("{$CFG->dirroot}/webservice/tests/helpers.php"); + +/** + * Unit tests of external class for re-ordering report filters + * + * @package core_reportbuilder + * @covers \core_reportbuilder\external\filters\reorder + * @copyright 2021 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class reorder_test extends externallib_advanced_testcase { + + /** + * Text execute method + */ + public function test_execute(): void { + $this->resetAfterTest(); + $this->setAdminUser(); + + /** @var core_reportbuilder_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); + + $report = $generator->create_report([ + 'name' => 'My report', + 'source' => users::class, + 'default' => false, + ]); + + // Add four filters. + $generator->create_filter(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:fullname']); + $generator->create_filter(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:email']); + $generator->create_filter(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:country']); + $filtercity = $generator->create_filter(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:city']); + + // Move the city filter to second position. + $result = reorder::execute($report->get('id'), $filtercity->get('id'), 2); + $result = external_api::clean_returnvalue(reorder::execute_returns(), $result); + $this->assertTrue($result['hasavailablefilters']); + $this->assertEquals('User', $result['availablefilters'][0]['optiongroup']['text']); + $this->assertNotEmpty($result['availablefilters'][0]['optiongroup']['values']); + $this->assertTrue($result['hasactivefilters']); + $this->assertCount(4, $result['activefilters']); + + // Assert report filters order. + $filters = filter::get_filter_records($report->get('id'), 'filterorder'); + + $filteridentifiers = array_map(static function(filter $filter): string { + return $filter->get('uniqueidentifier'); + }, $filters); + + $this->assertEquals([ + 'user:fullname', + 'user:city', + 'user:email', + 'user:country', + ], $filteridentifiers); + } + + /** + * Test execute method for a user without permission to edit reports + */ + public function test_execute_access_exception(): void { + $this->resetAfterTest(); + + /** @var core_reportbuilder_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); + + $report = $generator->create_report(['name' => 'My report', 'source' => users::class]); + $filter = $generator->create_filter(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:email']); + + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + + $this->expectException(report_access_exception::class); + $this->expectExceptionMessage('You can not edit this report'); + reorder::execute($report->get('id'), $filter->get('id'), 1); + } +} diff --git a/reportbuilder/tests/fixtures/testable_system_report_table.php b/reportbuilder/tests/fixtures/testable_system_report_table.php index f6f76ee5e4d..ff1495d9d03 100644 --- a/reportbuilder/tests/fixtures/testable_system_report_table.php +++ b/reportbuilder/tests/fixtures/testable_system_report_table.php @@ -40,7 +40,7 @@ class testable_system_report_table extends system_report_table { $record = parent::format_row($row); $result = []; - $columns = $this->systemreport->get_columns(); + $columns = $this->report->get_columns(); foreach ($columns as $column) { $result[$column->get_name()] = $record[$column->get_column_alias()]; } diff --git a/user/classes/reportbuilder/datasource/users.php b/user/classes/reportbuilder/datasource/users.php index 92a6e3eec01..c37eb9f5fce 100644 --- a/user/classes/reportbuilder/datasource/users.php +++ b/user/classes/reportbuilder/datasource/users.php @@ -61,6 +61,7 @@ class users extends datasource { $userentityname = $userentity->get_entity_name(); $this->add_columns_from_entity($userentityname); + $this->add_filters_from_entity($userentityname); $this->add_conditions_from_entity($userentityname); } @@ -73,6 +74,15 @@ class users extends datasource { return ['user:fullname', 'user:username', 'user:email']; } + /** + * Return the filters that will be added to the report once is created + * + * @return string[] + */ + public function get_default_filters(): array { + return ['user:fullname', 'user:username', 'user:email']; + } + /** * Return the conditions that will be added to the report once is created *