diff --git a/lang/en/reportbuilder.php b/lang/en/reportbuilder.php index 726238baae4..779ed884362 100644 --- a/lang/en/reportbuilder.php +++ b/lang/en/reportbuilder.php @@ -23,6 +23,7 @@ */ $string['actions'] = 'Actions'; +$string['addcolumn'] = 'Add column \'{$a}\''; $string['apply'] = 'Apply'; $string['coursefullnamewithlink'] = 'Course full name with link'; $string['courseidnumberewithlink'] = 'Course ID number with link'; @@ -30,9 +31,14 @@ $string['courseshortnamewithlink'] = 'Course short name with link'; $string['customfieldcolumn'] = '{$a}'; $string['deletereport'] = 'Delete report'; $string['deletereportconfirm'] = 'Are you sure you want to delete the report \'{$a}\' and all associated data?'; +$string['editdetails'] = 'Edit details'; +$string['editreportcontent'] = 'Edit report content'; $string['editreportdetails'] = 'Edit report details'; $string['editreportname'] = 'Edit report name'; +$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['entitycourse'] = 'Course'; $string['entityuser'] = 'User'; $string['errorreportaccess'] = 'You can not view this report'; @@ -73,6 +79,7 @@ $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['newreport'] = 'New report'; $string['privacy:metadata:column'] = 'Report column definitions'; $string['privacy:metadata:column:uniqueidentifier'] = 'Unique identifier of the column'; @@ -87,15 +94,20 @@ $string['privacy:metadata:report'] = 'Report definitions'; $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['reportbuilder'] = 'Report builder'; $string['reportcreated'] = 'Report created'; $string['reportdeleted'] = 'Report deleted'; +$string['reportsettingstoggle'] = 'Show/hide settings sidebar'; $string['reportsource'] = 'Report source'; $string['reportsource_help'] = 'The report source defines where the data for the report will come from'; $string['reportupdated'] = 'Report updated'; $string['resetall'] = 'Reset all'; $string['selectareportsource'] = 'Select a report source'; $string['selectcourses'] = 'Select courses'; +$string['showhide'] = 'Show/hide \'{$a}\''; +$string['switchedit'] = 'Switch to edit mode'; +$string['switchpreview'] = 'Switch to preview mode'; $string['timeadded'] = 'Time added'; $string['timecreated'] = 'Time created'; $string['timemodified'] = 'Time modified'; diff --git a/lib/db/services.php b/lib/db/services.php index 7daf1f23503..f69f212e64a 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -2804,6 +2804,84 @@ $functions = array( 'type' => 'write', 'ajax' => true, ], + 'core_reportbuilder_reports_get' => [ + 'classname' => 'core_reportbuilder\external\reports\get', + 'description' => 'Get custom report', + 'type' => 'read', + 'ajax' => true, + ], + 'core_reportbuilder_columns_add' => [ + 'classname' => 'core_reportbuilder\external\columns\add', + 'description' => 'Add column to report', + 'type' => 'write', + 'ajax' => true, + ], + 'core_reportbuilder_columns_delete' => [ + 'classname' => 'core_reportbuilder\external\columns\delete', + 'description' => 'Delete column from report', + 'type' => 'write', + 'ajax' => true, + ], + 'core_reportbuilder_columns_reorder' => [ + 'classname' => 'core_reportbuilder\external\columns\reorder', + 'description' => 'Re-order column within report', + 'type' => 'write', + 'ajax' => true, + ], + 'core_reportbuilder_columns_sort_reorder' => [ + 'classname' => 'core_reportbuilder\external\columns\sort\reorder', + 'description' => 'Re-order column sorting within report', + 'type' => 'write', + 'ajax' => true, + ], + 'core_reportbuilder_columns_sort_toggle' => [ + 'classname' => 'core_reportbuilder\external\columns\sort\toggle', + 'description' => 'Toggle sorting of column within report', + 'type' => 'write', + 'ajax' => true, + ], + 'core_reportbuilder_conditions_add' => [ + 'classname' => 'core_reportbuilder\external\conditions\add', + 'description' => 'Add condition to report', + 'type' => 'write', + 'ajax' => true, + ], + 'core_reportbuilder_conditions_delete' => [ + 'classname' => 'core_reportbuilder\external\conditions\delete', + 'description' => 'Delete condition from report', + 'type' => 'write', + 'ajax' => true, + ], + 'core_reportbuilder_conditions_reorder' => [ + 'classname' => 'core_reportbuilder\external\conditions\reorder', + 'description' => 'Re-order condition within report', + 'type' => 'write', + 'ajax' => true, + ], + 'core_reportbuilder_conditions_reset' => [ + 'classname' => 'core_reportbuilder\external\conditions\reset', + 'description' => 'Reset conditions for given report', + 'type' => 'write', + 'ajax' => true, + ], + 'core_reportbuilder_filters_add' => [ + 'classname' => 'core_reportbuilder\external\filters\add', + 'description' => 'Add filter to report', + 'type' => 'write', + 'ajax' => true, + ], + 'core_reportbuilder_filters_delete' => [ + 'classname' => 'core_reportbuilder\external\filters\delete', + 'description' => 'Delete filter from report', + 'type' => 'write', + 'ajax' => true, + ], + 'core_reportbuilder_filters_reorder' => [ + 'classname' => 'core_reportbuilder\external\filters\reorder', + 'description' => 'Re-order filter within report', + 'type' => 'write', + 'ajax' => true, + ], ); $services = array( diff --git a/reportbuilder/amd/build/editor.min.js b/reportbuilder/amd/build/editor.min.js new file mode 100644 index 00000000000..53a119c1704 --- /dev/null +++ b/reportbuilder/amd/build/editor.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/editor",["exports","core/inplace_editable","core/notification","core/pending","core/templates","core_reportbuilder/local/selectors","core_reportbuilder/local/editor/columns","core_reportbuilder/local/repository/reports"],function(a,b,c,d,e,f,g,h){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=void 0;c=k(c);d=k(d);e=k(e);f=j(f);function i(){if("function"!=typeof WeakMap)return null;var a=new WeakMap;i=function(){return a};return a}function j(a){if(a&&a.__esModule){return a}if(null===a||"object"!==_typeof(a)&&"function"!=typeof a){return{default:a}}var b=i();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 k(a){return a&&a.__esModule?a:{default:a}}var l=!1,m=function(){var a=document.querySelector(f.regions.report);(0,g.init)(a,l);if(l){return}document.addEventListener("click",function(b){var g=b.target.closest(f.actions.toggleEditPreview);if(g){b.preventDefault();var i=new d.default("core_reportbuilder/reports:get"),j="1"!==g.dataset.editMode,k="";(0,h.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 i.resolve()}).catch(c.default.exception)}});l=!0};a.init=m}); +//# sourceMappingURL=editor.min.js.map diff --git a/reportbuilder/amd/build/editor.min.js.map b/reportbuilder/amd/build/editor.min.js.map new file mode 100644 index 00000000000..8a79d5aca63 --- /dev/null +++ b/reportbuilder/amd/build/editor.min.js.map @@ -0,0 +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":"sjBAwBA,a,+DAGA,OACA,OACA,OACA,O,4lBAIIA,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,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 {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\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/filters.min.js b/reportbuilder/amd/build/filters.min.js index cb9aeb7436d..84792e00fc9 100644 --- a/reportbuilder/amd/build/filters.min.js +++ b/reportbuilder/amd/build/filters.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/filters",["exports","core/event_dispatcher","core/fragment","core/notification","core/pending","core/str","core/templates","core/toast","core_form/dynamicform","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){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=void 0;d=o(d);e=o(e);g=o(g);i=o(i);j=n(j);k=n(k);function m(){if("function"!=typeof WeakMap)return null;var a=new WeakMap;m=function(){return a};return a}function n(a){if(a&&a.__esModule){return a}if(null===a||"object"!==_typeof(a)&&"function"!=typeof a){return{default:a}}var b=m();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 o(a){return a&&a.__esModule?a:{default:a}}function p(a,b,c,d,e,f,g){try{var h=a[f](g),i=h.value}catch(a){c(a);return}if(h.done){b(i)}else{Promise.resolve(i).then(d,e)}}function q(a){return function(){var b=this,c=arguments;return new Promise(function(d,e){var h=a.apply(b,c);function f(a){p(h,d,e,f,g,"next",a)}function g(a){p(h,d,e,f,g,"throw",a)}f(void 0)})}}var r=function(){var a=q(regeneratorRuntime.mark(function a(b,c){var d;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:d=b.querySelector(k.regions.filterButtonLabel);if(!(0.\n\n/**\n * Report builder filter management\n *\n * @module core_reportbuilder/filters\n * @copyright 2021 Paul Holden \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {dispatchEvent} from 'core/event_dispatcher';\nimport {loadFragment} from 'core/fragment';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\nimport {get_string as getString} from 'core/str';\nimport Templates from 'core/templates';\nimport {add as addToast} from 'core/toast';\nimport DynamicForm from 'core_form/dynamicform';\nimport * as reportEvents from 'core_reportbuilder/local/events';\nimport * as reportSelectors from 'core_reportbuilder/local/selectors';\nimport {reset as resetFilters} from 'core_reportbuilder/local/repository/filters';\n\n/**\n * Update filter button text to indicate applied filter count\n *\n * @param {Element} reportElement\n * @param {Number} filterCount\n */\nconst setFilterButtonCount = async(reportElement, filterCount) => {\n const filterButtonLabel = reportElement.querySelector(reportSelectors.regions.filterButtonLabel);\n\n if (filterCount > 0) {\n filterButtonLabel.textContent = await getString('filtersappliedx', 'core_reportbuilder', filterCount);\n } else {\n filterButtonLabel.textContent = await getString('filters', 'moodle');\n }\n};\n\n/**\n * Initialise module for given report\n *\n * @method\n * @param {Number} reportId\n * @param {Number} contextId\n */\nexport const init = (reportId, contextId) => {\n const reportElement = document.querySelector(reportSelectors.forSystemReport(reportId));\n const filterFormContainer = reportElement.querySelector(reportSelectors.regions.filtersForm);\n const filterForm = new DynamicForm(filterFormContainer, '\\\\core_reportbuilder\\\\form\\\\filter');\n\n // Submit report filters.\n filterForm.addEventListener(filterForm.events.FORM_SUBMITTED, event => {\n event.preventDefault();\n\n // After the form has been submitted, we should trigger report table reload.\n dispatchEvent(reportEvents.tableReload, {}, reportElement);\n setFilterButtonCount(reportElement, event.detail);\n\n getString('filtersapplied', 'core_reportbuilder')\n .then(addToast)\n .catch(Notification.exception);\n });\n\n // Reset report filters.\n filterForm.addEventListener(filterForm.events.NOSUBMIT_BUTTON_PRESSED, event => {\n event.preventDefault();\n\n const pendingPromise = new Pending('core_reportbuilder/filters:reset');\n\n resetFilters(reportId)\n .then(() => getString('filtersreset', 'core_reportbuilder'))\n .then(addToast)\n .then(() => loadFragment('core_reportbuilder', 'filters_form', contextId, {\n reportid: reportId,\n parameters: reportElement.dataset.parameter,\n }))\n .then((html, js) => {\n Templates.replaceNodeContents(filterFormContainer, html, js);\n\n dispatchEvent(reportEvents.tableReload, {}, reportElement);\n setFilterButtonCount(reportElement, 0);\n\n return pendingPromise.resolve();\n })\n .catch(Notification.exception);\n });\n\n // Modify \"region-main\" overflow for big filter forms.\n document.querySelector('#region-main').style.overflowX = \"visible\";\n};\n"],"file":"filters.min.js"} \ No newline at end of file +{"version":3,"sources":["../src/filters.js"],"names":["setFilterButtonCount","reportElement","filterCount","filterButtonLabel","querySelector","reportSelectors","regions","textContent","init","reportId","contextId","document","forReport","filterFormContainer","filtersForm","filterForm","DynamicForm","addEventListener","events","FORM_SUBMITTED","event","preventDefault","reportEvents","tableReload","detail","then","addToast","catch","Notification","exception","NOSUBMIT_BUTTON_PRESSED","pendingPromise","Pending","reportid","parameters","dataset","parameter","html","js","Templates","replaceNodeContents","resolve","style","overflowX"],"mappings":"msBAyBA,OACA,OAEA,OAEA,OACA,OACA,O,25BASMA,CAAAA,CAAoB,4CAAG,WAAMC,CAAN,CAAqBC,CAArB,yFACnBC,CADmB,CACCF,CAAa,CAACG,aAAd,CAA4BC,CAAe,CAACC,OAAhB,CAAwBH,iBAApD,CADD,MAGP,CAAd,CAAAD,CAHqB,iCAIiB,iBAAU,iBAAV,CAA6B,oBAA7B,CAAmDA,CAAnD,CAJjB,QAIrBC,CAAiB,CAACI,WAJG,8CAMiB,iBAAU,SAAV,CAAqB,QAArB,CANjB,QAMrBJ,CAAiB,CAACI,WANG,iDAAH,uD,CAiBbC,CAAI,CAAG,SAACC,CAAD,CAAWC,CAAX,CAAyB,IACnCT,CAAAA,CAAa,CAAGU,QAAQ,CAACP,aAAT,CAAuBC,CAAe,CAACO,SAAhB,CAA0BH,CAA1B,CAAvB,CADmB,CAEnCI,CAAmB,CAAGZ,CAAa,CAACG,aAAd,CAA4BC,CAAe,CAACC,OAAhB,CAAwBQ,WAApD,CAFa,CAGnCC,CAAU,CAAG,GAAIC,UAAJ,CAAgBH,CAAhB,CAAqC,oCAArC,CAHsB,CAMzCE,CAAU,CAACE,gBAAX,CAA4BF,CAAU,CAACG,MAAX,CAAkBC,cAA9C,CAA8D,SAAAC,CAAK,CAAI,CACnEA,CAAK,CAACC,cAAN,GAGA,oBAAcC,CAAY,CAACC,WAA3B,CAAwC,EAAxC,CAA4CtB,CAA5C,EACAD,CAAoB,CAACC,CAAD,CAAgBmB,CAAK,CAACI,MAAtB,CAApB,CAEA,iBAAU,gBAAV,CAA4B,oBAA5B,EACKC,IADL,CACUC,KADV,EAEKC,KAFL,CAEWC,UAAaC,SAFxB,CAGH,CAVD,EAaAd,CAAU,CAACE,gBAAX,CAA4BF,CAAU,CAACG,MAAX,CAAkBY,uBAA9C,CAAuE,SAAAV,CAAK,CAAI,CAC5EA,CAAK,CAACC,cAAN,GAEA,GAAMU,CAAAA,CAAc,CAAG,GAAIC,UAAJ,CAAY,kCAAZ,CAAvB,CAEA,mBAAavB,CAAb,EACKgB,IADL,CACU,iBAAM,iBAAU,cAAV,CAA0B,oBAA1B,CAAN,CADV,EAEKA,IAFL,CAEUC,KAFV,EAGKD,IAHL,CAGU,iBAAM,mBAAa,oBAAb,CAAmC,cAAnC,CAAmDf,CAAnD,CAA8D,CACtEuB,QAAQ,CAAExB,CAD4D,CAEtEyB,UAAU,CAAEjC,CAAa,CAACkC,OAAd,CAAsBC,SAFoC,CAA9D,CAAN,CAHV,EAOKX,IAPL,CAOU,SAACY,CAAD,CAAOC,CAAP,CAAc,CAChBC,UAAUC,mBAAV,CAA8B3B,CAA9B,CAAmDwB,CAAnD,CAAyDC,CAAzD,EAEA,oBAAchB,CAAY,CAACC,WAA3B,CAAwC,EAAxC,CAA4CtB,CAA5C,EACAD,CAAoB,CAACC,CAAD,CAAgB,CAAhB,CAApB,CAEA,MAAO8B,CAAAA,CAAc,CAACU,OAAf,EACV,CAdL,EAeKd,KAfL,CAeWC,UAAaC,SAfxB,CAgBH,CArBD,EAwBAlB,QAAQ,CAACP,aAAT,CAAuB,cAAvB,EAAuCsC,KAAvC,CAA6CC,SAA7C,CAAyD,SAC5D,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 filter management\n *\n * @module core_reportbuilder/filters\n * @copyright 2021 Paul Holden \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {dispatchEvent} from 'core/event_dispatcher';\nimport {loadFragment} from 'core/fragment';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\nimport {get_string as getString} from 'core/str';\nimport Templates from 'core/templates';\nimport {add as addToast} from 'core/toast';\nimport DynamicForm from 'core_form/dynamicform';\nimport * as reportEvents from 'core_reportbuilder/local/events';\nimport * as reportSelectors from 'core_reportbuilder/local/selectors';\nimport {resetFilters} from 'core_reportbuilder/local/repository/filters';\n\n/**\n * Update filter button text to indicate applied filter count\n *\n * @param {Element} reportElement\n * @param {Number} filterCount\n */\nconst setFilterButtonCount = async(reportElement, filterCount) => {\n const filterButtonLabel = reportElement.querySelector(reportSelectors.regions.filterButtonLabel);\n\n if (filterCount > 0) {\n filterButtonLabel.textContent = await getString('filtersappliedx', 'core_reportbuilder', filterCount);\n } else {\n filterButtonLabel.textContent = await getString('filters', 'moodle');\n }\n};\n\n/**\n * Initialise module for given report\n *\n * @method\n * @param {Number} reportId\n * @param {Number} contextId\n */\nexport const init = (reportId, contextId) => {\n const reportElement = document.querySelector(reportSelectors.forReport(reportId));\n const filterFormContainer = reportElement.querySelector(reportSelectors.regions.filtersForm);\n const filterForm = new DynamicForm(filterFormContainer, '\\\\core_reportbuilder\\\\form\\\\filter');\n\n // Submit report filters.\n filterForm.addEventListener(filterForm.events.FORM_SUBMITTED, event => {\n event.preventDefault();\n\n // After the form has been submitted, we should trigger report table reload.\n dispatchEvent(reportEvents.tableReload, {}, reportElement);\n setFilterButtonCount(reportElement, event.detail);\n\n getString('filtersapplied', 'core_reportbuilder')\n .then(addToast)\n .catch(Notification.exception);\n });\n\n // Reset report filters.\n filterForm.addEventListener(filterForm.events.NOSUBMIT_BUTTON_PRESSED, event => {\n event.preventDefault();\n\n const pendingPromise = new Pending('core_reportbuilder/filters:reset');\n\n resetFilters(reportId)\n .then(() => getString('filtersreset', 'core_reportbuilder'))\n .then(addToast)\n .then(() => loadFragment('core_reportbuilder', 'filters_form', contextId, {\n reportid: reportId,\n parameters: reportElement.dataset.parameter,\n }))\n .then((html, js) => {\n Templates.replaceNodeContents(filterFormContainer, html, js);\n\n dispatchEvent(reportEvents.tableReload, {}, reportElement);\n setFilterButtonCount(reportElement, 0);\n\n return pendingPromise.resolve();\n })\n .catch(Notification.exception);\n });\n\n // Modify \"region-main\" overflow for big filter forms.\n document.querySelector('#region-main').style.overflowX = \"visible\";\n};\n"],"file":"filters.min.js"} \ No newline at end of file diff --git a/reportbuilder/amd/build/local/editor/columns.min.js b/reportbuilder/amd/build/local/editor/columns.min.js new file mode 100644 index 00000000000..3326bf362f7 --- /dev/null +++ b/reportbuilder/amd/build/local/editor/columns.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/columns",["exports","jquery","core/event_dispatcher","core/inplace_editable","core/notification","core/pending","core/pubsub","core/sortable_list","core/str","core/toast","core_reportbuilder/local/events","core_reportbuilder/local/selectors","core_reportbuilder/local/repository/columns"],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);h=p(h);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);cn){o--}(0,m.reorderColumn)(a.dataset.reportId,h,o).then(function(){return(0,i.get_string)("columnmoved","core_reportbuilder",l)}).then(j.add).then(function(){(0,c.dispatchEvent)(k.tableReload,{preservePagination:!0},a);return g.resolve()}).catch(e.default.exception)}})};a.init=w}); +//# sourceMappingURL=columns.min.js.map diff --git a/reportbuilder/amd/build/local/editor/columns.min.js.map b/reportbuilder/amd/build/local/editor/columns.min.js.map new file mode 100644 index 00000000000..102b737e28e --- /dev/null +++ b/reportbuilder/amd/build/local/editor/columns.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../../src/local/editor/columns.js"],"names":["init","reportElement","initialized","addEventListener","event","reportAddColumn","target","closest","reportSelectors","actions","preventDefault","pendingPromise","Pending","dataset","reportId","uniqueIdentifier","then","data","reportEvents","publish","reportColumnsUpdated","name","addToast","tableReload","preservePagination","resolve","catch","Notification","exception","reportRemoveColumn","columnHeader","regions","columnName","key","component","param","confirmTitle","confirmText","confirmButton","confirm","columnId","columnSortableList","SortableList","reportTable","isHorizontal","getElementName","element","Promise","on","EVENTS","DRAG","info","columnPosition","targetColumnPosition","targetNextElement","find","each","cell","children","beforeCell","insertBefore","appendChild","DROP","positionChanged","siblings","length"],"mappings":"ipBAwBA,a,+DAEA,OAGA,OACA,OAEA,OAGA,OACA,O,qjDASO,GAAMA,CAAAA,CAAI,CAAG,SAACC,CAAD,CAAgBC,CAAhB,CAAgC,CAChD,GAAIA,CAAJ,CAAiB,CACb,MACH,CAEDD,CAAa,CAACE,gBAAd,CAA+B,OAA/B,CAAwC,SAAAC,CAAK,CAAI,CAG7C,GAAMC,CAAAA,CAAe,CAAGD,CAAK,CAACE,MAAN,CAAaC,OAAb,CAAqBC,CAAe,CAACC,OAAhB,CAAwBJ,eAA7C,CAAxB,CACA,GAAIA,CAAJ,CAAqB,CACjBD,CAAK,CAACM,cAAN,GAEA,GAAMC,CAAAA,CAAc,CAAG,GAAIC,UAAJ,CAAY,gCAAZ,CAAvB,CAEA,gBAAUX,CAAa,CAACY,OAAd,CAAsBC,QAAhC,CAA0CT,CAAe,CAACQ,OAAhB,CAAwBE,gBAAlE,EACKC,IADL,CACU,SAAAC,CAAI,QAAI,cAAQC,CAAY,CAACC,OAAb,CAAqBC,oBAA7B,CAAmDH,CAAnD,CAAJ,CADd,EAEKD,IAFL,CAEU,iBAAM,iBAAU,aAAV,CAAyB,oBAAzB,CAA+CX,CAAe,CAACQ,OAAhB,CAAwBQ,IAAvE,CAAN,CAFV,EAGKL,IAHL,CAGUM,KAHV,EAIKN,IAJL,CAIU,UAAM,CACR,oBAAcE,CAAY,CAACK,WAA3B,CAAwC,CAACC,kBAAkB,GAAnB,CAAxC,CAAoEvB,CAApE,EACA,MAAOU,CAAAA,CAAc,CAACc,OAAf,EACV,CAPL,EAQKC,KARL,CAQWC,UAAaC,SARxB,CASH,CAGD,GAAMC,CAAAA,CAAkB,CAAGzB,CAAK,CAACE,MAAN,CAAaC,OAAb,CAAqBC,CAAe,CAACC,OAAhB,CAAwBoB,kBAA7C,CAA3B,CACA,GAAIA,CAAJ,CAAwB,CACpBzB,CAAK,CAACM,cAAN,GADoB,GAGdoB,CAAAA,CAAY,CAAGD,CAAkB,CAACtB,OAAnB,CAA2BC,CAAe,CAACuB,OAAhB,CAAwBD,YAAnD,CAHD,CAIdE,CAAU,CAAGF,CAAY,CAACjB,OAAb,CAAqBmB,UAJpB,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,EAIGlB,IAJH,CAIQ,WAAgD,cAA9CoB,CAA8C,MAAhCC,CAAgC,MAAnBC,CAAmB,MACpDX,UAAaY,OAAb,CAAqBH,CAArB,CAAmCC,CAAnC,CAAgDC,CAAhD,CAA+D,IAA/D,CAAqE,UAAM,CACvE,GAAM3B,CAAAA,CAAc,CAAG,GAAIC,UAAJ,CAAY,mCAAZ,CAAvB,CAEA,mBAAaX,CAAa,CAACY,OAAd,CAAsBC,QAAnC,CAA6CgB,CAAY,CAACjB,OAAb,CAAqB2B,QAAlE,EACKxB,IADL,CACU,SAAAC,CAAI,QAAI,cAAQC,CAAY,CAACC,OAAb,CAAqBC,oBAA7B,CAAmDH,CAAnD,CAAJ,CADd,EAEKD,IAFL,CAEU,iBAAM,iBAAU,eAAV,CAA2B,oBAA3B,CAAiDgB,CAAjD,CAAN,CAFV,EAGKhB,IAHL,CAGUM,KAHV,EAIKN,IAJL,CAIU,UAAM,CACR,oBAAcE,CAAY,CAACK,WAA3B,CAAwC,CAACC,kBAAkB,GAAnB,CAAxC,CAAoEvB,CAApE,EACA,MAAOU,CAAAA,CAAc,CAACc,OAAf,EACV,CAPL,EAQKC,KARL,CAQWC,UAAaC,SARxB,CASH,CAZD,CAcH,CAnBD,EAmBGF,KAnBH,CAmBSC,UAAaC,SAnBtB,CAoBH,CACJ,CAjDD,EAoDA,GAAIa,CAAAA,CAAkB,CAAG,GAAIC,UAAJ,WAAoBlC,CAAe,CAACuB,OAAhB,CAAwBY,WAA5C,cAAoE,CAACC,YAAY,GAAb,CAApE,CAAzB,CACAH,CAAkB,CAACI,cAAnB,CAAoC,SAAAC,CAAO,QAAIC,CAAAA,OAAO,CAACtB,OAAR,CAAgBqB,CAAO,CAAC7B,IAAR,CAAa,YAAb,CAAhB,CAAJ,CAA3C,CAEA,cAAEhB,CAAF,EAAiB+C,EAAjB,CAAoBN,UAAaO,MAAb,CAAoBC,IAAxC,CAA8C,oBAA9C,CAAoE,SAAC9C,CAAD,CAAQ+C,CAAR,CAAiB,IAC3EC,CAAAA,CAAc,CAAGD,CAAI,CAACL,OAAL,CAAa7B,IAAb,CAAkB,gBAAlB,CAD0D,CAE3EoC,CAAoB,CAAGF,CAAI,CAACG,iBAAL,CAAuBrC,IAAvB,CAA4B,gBAA5B,CAFoD,CAIjF,cAAEhB,CAAF,EAAiBsD,IAAjB,CAAsB,UAAtB,EAAkCC,IAAlC,CAAuC,UAAW,CAC9C,GAAMC,CAAAA,CAAI,CAAG,cAAE,IAAF,EAAQC,QAAR,eAAwBN,CAAc,CAAG,CAAzC,GAA8C,CAA9C,CAAb,CACA,GAAIC,CAAJ,CAA0B,CACtB,GAAIM,CAAAA,CAAU,CAAG,cAAE,IAAF,EAAQD,QAAR,eAAwBL,CAAoB,CAAG,CAA/C,GAAoD,CAApD,CAAjB,CACA,KAAKO,YAAL,CAAkBH,CAAlB,CAAwBE,CAAxB,CACH,CAHD,IAGO,CACH,KAAKE,WAAL,CAAiBJ,CAAjB,CACH,CACJ,CARD,CASH,CAbD,EAeA,cAAExD,CAAF,EAAiB+C,EAAjB,CAAoBN,UAAaO,MAAb,CAAoBa,IAAxC,CAA8C,oBAA9C,CAAoE,SAAC1D,CAAD,CAAQ+C,CAAR,CAAiB,CACjF,GAAIA,CAAI,CAACY,eAAT,CAA0B,IAChBpD,CAAAA,CAAc,CAAG,GAAIC,UAAJ,CAAY,oCAAZ,CADD,CAGhB4B,CAAQ,CAAGW,CAAI,CAACL,OAAL,CAAa7B,IAAb,CAAkB,UAAlB,CAHK,CAIhBe,CAAU,CAAGmB,CAAI,CAACL,OAAL,CAAa7B,IAAb,CAAkB,YAAlB,CAJG,CAKhBmC,CAAc,CAAGD,CAAI,CAACL,OAAL,CAAa7B,IAAb,CAAkB,gBAAlB,CALD,CAQlBoC,CAAoB,CAAGF,CAAI,CAACG,iBAAL,CAAuBrC,IAAvB,CAA4B,gBAA5B,GAAiDkC,CAAI,CAACL,OAAL,CAAakB,QAAb,GAAwBC,MAAxB,CAAiC,CARvF,CAStB,GAAIZ,CAAoB,CAAGD,CAA3B,CAA2C,CACvCC,CAAoB,EACvB,CAED,oBAAcpD,CAAa,CAACY,OAAd,CAAsBC,QAApC,CAA8C0B,CAA9C,CAAwDa,CAAxD,EACKrC,IADL,CACU,iBAAM,iBAAU,aAAV,CAAyB,oBAAzB,CAA+CgB,CAA/C,CAAN,CADV,EAEKhB,IAFL,CAEUM,KAFV,EAGKN,IAHL,CAGU,UAAM,CACR,oBAAcE,CAAY,CAACK,WAA3B,CAAwC,CAACC,kBAAkB,GAAnB,CAAxC,CAAoEvB,CAApE,EACA,MAAOU,CAAAA,CAAc,CAACc,OAAf,EACV,CANL,EAOKC,KAPL,CAOWC,UAAaC,SAPxB,CAQH,CACJ,CAvBD,CAwBH,CAnGM,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 columns editor\n *\n * @module core_reportbuilder/local/editor/columns\n * @package core_reportbuilder\n * @copyright 2021 Paul Holden \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 {publish} from 'core/pubsub';\nimport SortableList from 'core/sortable_list';\nimport {get_string as getString, get_strings as getStrings} from 'core/str';\nimport {add as addToast} from 'core/toast';\nimport * as reportEvents from 'core_reportbuilder/local/events';\nimport * as reportSelectors from 'core_reportbuilder/local/selectors';\nimport {addColumn, deleteColumn, reorderColumn} from 'core_reportbuilder/local/repository/columns';\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 column to report.\n const reportAddColumn = event.target.closest(reportSelectors.actions.reportAddColumn);\n if (reportAddColumn) {\n event.preventDefault();\n\n const pendingPromise = new Pending('core_reportbuilder/columns:add');\n\n addColumn(reportElement.dataset.reportId, reportAddColumn.dataset.uniqueIdentifier)\n .then(data => publish(reportEvents.publish.reportColumnsUpdated, data))\n .then(() => getString('columnadded', 'core_reportbuilder', reportAddColumn.dataset.name))\n .then(addToast)\n .then(() => {\n dispatchEvent(reportEvents.tableReload, {preservePagination: true}, reportElement);\n return pendingPromise.resolve();\n })\n .catch(Notification.exception);\n }\n\n // Remove column from report.\n const reportRemoveColumn = event.target.closest(reportSelectors.actions.reportRemoveColumn);\n if (reportRemoveColumn) {\n event.preventDefault();\n\n const columnHeader = reportRemoveColumn.closest(reportSelectors.regions.columnHeader);\n const columnName = columnHeader.dataset.columnName;\n\n getStrings([\n {key: 'deletecolumn', component: 'core_reportbuilder', param: columnName},\n {key: 'deletecolumnconfirm', component: 'core_reportbuilder', param: columnName},\n {key: 'delete', component: 'moodle'},\n ]).then(([confirmTitle, confirmText, confirmButton]) => {\n Notification.confirm(confirmTitle, confirmText, confirmButton, null, () => {\n const pendingPromise = new Pending('core_reportbuilder/columns:remove');\n\n deleteColumn(reportElement.dataset.reportId, columnHeader.dataset.columnId)\n .then(data => publish(reportEvents.publish.reportColumnsUpdated, data))\n .then(() => getString('columndeleted', 'core_reportbuilder', columnName))\n .then(addToast)\n .then(() => {\n dispatchEvent(reportEvents.tableReload, {preservePagination: true}, 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 column moving (note JQuery dependency, see MDL-72293 for resolution).\n var columnSortableList = new SortableList(`${reportSelectors.regions.reportTable} thead tr`, {isHorizontal: true});\n columnSortableList.getElementName = element => Promise.resolve(element.data('columnName'));\n\n $(reportElement).on(SortableList.EVENTS.DRAG, 'th[data-column-id]', (event, info) => {\n const columnPosition = info.element.data('columnPosition');\n const targetColumnPosition = info.targetNextElement.data('columnPosition');\n\n $(reportElement).find('tbody tr').each(function() {\n const cell = $(this).children(`td.c${columnPosition - 1}`)[0];\n if (targetColumnPosition) {\n var beforeCell = $(this).children(`td.c${targetColumnPosition - 1}`)[0];\n this.insertBefore(cell, beforeCell);\n } else {\n this.appendChild(cell);\n }\n });\n });\n\n $(reportElement).on(SortableList.EVENTS.DROP, 'th[data-column-id]', (event, info) => {\n if (info.positionChanged) {\n const pendingPromise = new Pending('core_reportbuilder/columns:reorder');\n\n const columnId = info.element.data('columnId');\n const columnName = info.element.data('columnName');\n const columnPosition = info.element.data('columnPosition');\n\n // Select target position, if moving to the end then count number of element siblings.\n let targetColumnPosition = info.targetNextElement.data('columnPosition') || info.element.siblings().length + 2;\n if (targetColumnPosition > columnPosition) {\n targetColumnPosition--;\n }\n\n reorderColumn(reportElement.dataset.reportId, columnId, targetColumnPosition)\n .then(() => getString('columnmoved', 'core_reportbuilder', columnName))\n .then(addToast)\n .then(() => {\n dispatchEvent(reportEvents.tableReload, {preservePagination: true}, reportElement);\n return pendingPromise.resolve();\n })\n .catch(Notification.exception);\n }\n });\n};\n"],"file":"columns.min.js"} \ No newline at end of file diff --git a/reportbuilder/amd/build/local/repository/columns.min.js b/reportbuilder/amd/build/local/repository/columns.min.js new file mode 100644 index 00000000000..f660ccb3236 --- /dev/null +++ b/reportbuilder/amd/build/local/repository/columns.min.js @@ -0,0 +1,2 @@ +define ("core_reportbuilder/local/repository/columns",["exports","core/ajax"],function(a,b){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.reorderColumn=a.deleteColumn=a.addColumn=void 0;b=function(a){return a&&a.__esModule?a:{default:a}}(b);var c=function(a,c){return b.default.call([{methodname:"core_reportbuilder_columns_add",args:{reportid:a,uniqueidentifier:c}}])[0]};a.addColumn=c;var d=function(a,c){return b.default.call([{methodname:"core_reportbuilder_columns_delete",args:{reportid:a,columnid:c}}])[0]};a.deleteColumn=d;var e=function(a,c,d){return b.default.call([{methodname:"core_reportbuilder_columns_reorder",args:{reportid:a,columnid:c,position:d}}])[0]};a.reorderColumn=e}); +//# sourceMappingURL=columns.min.js.map diff --git a/reportbuilder/amd/build/local/repository/columns.min.js.map b/reportbuilder/amd/build/local/repository/columns.min.js.map new file mode 100644 index 00000000000..c0628a133bd --- /dev/null +++ b/reportbuilder/amd/build/local/repository/columns.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../../src/local/repository/columns.js"],"names":["addColumn","reportId","uniqueIdentifier","Ajax","call","methodname","args","reportid","uniqueidentifier","deleteColumn","columnId","columnid","reorderColumn","position"],"mappings":"4MAwBA,uDASO,GAAMA,CAAAA,CAAS,CAAG,SAACC,CAAD,CAAWC,CAAX,CAAgC,CAMrD,MAAOC,WAAKC,IAAL,CAAU,CALD,CACZC,UAAU,CAAE,gCADA,CAEZC,IAAI,CAAE,CAACC,QAAQ,CAAEN,CAAX,CAAqBO,gBAAgB,CAAEN,CAAvC,CAFM,CAKC,CAAV,EAAqB,CAArB,CACV,CAPM,C,cAgBA,GAAMO,CAAAA,CAAY,CAAG,SAACR,CAAD,CAAWS,CAAX,CAAwB,CAMhD,MAAOP,WAAKC,IAAL,CAAU,CALD,CACZC,UAAU,CAAE,mCADA,CAEZC,IAAI,CAAE,CAACC,QAAQ,CAAEN,CAAX,CAAqBU,QAAQ,CAAED,CAA/B,CAFM,CAKC,CAAV,EAAqB,CAArB,CACV,CAPM,C,iBAiBA,GAAME,CAAAA,CAAa,CAAG,SAACX,CAAD,CAAWS,CAAX,CAAqBG,CAArB,CAAkC,CAM3D,MAAOV,WAAKC,IAAL,CAAU,CALD,CACZC,UAAU,CAAE,oCADA,CAEZC,IAAI,CAAE,CAACC,QAAQ,CAAEN,CAAX,CAAqBU,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 column AJAX requests\n *\n * @module core_reportbuilder/local/repository/columns\n * @package core_reportbuilder\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 * Add column to given report\n *\n * @param {Number} reportId\n * @param {String} uniqueIdentifier\n * @return {Promise}\n */\nexport const addColumn = (reportId, uniqueIdentifier) => {\n const request = {\n methodname: 'core_reportbuilder_columns_add',\n args: {reportid: reportId, uniqueidentifier: uniqueIdentifier}\n };\n\n return Ajax.call([request])[0];\n};\n\n/**\n * Remove column from given report\n *\n * @param {Number} reportId\n * @param {Number} columnId\n * @return {Promise}\n */\nexport const deleteColumn = (reportId, columnId) => {\n const request = {\n methodname: 'core_reportbuilder_columns_delete',\n args: {reportid: reportId, columnid: columnId}\n };\n\n return Ajax.call([request])[0];\n};\n\n/**\n * Re-order column within a report\n *\n * @param {Number} reportId\n * @param {Number} columnId\n * @param {Number} position\n * @return {Promise}\n */\nexport const reorderColumn = (reportId, columnId, position) => {\n const request = {\n methodname: 'core_reportbuilder_columns_reorder',\n args: {reportid: reportId, columnid: columnId, position: position}\n };\n\n return Ajax.call([request])[0];\n};\n"],"file":"columns.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 c68a07eddac..e1c7f17cc27 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.reset=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.reset=c}); +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}); //# 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 9dea17e3682..af457d5f121 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":["reset","reportId","Ajax","call","methodname","args","reportid"],"mappings":"yKAuBA,uDASO,GAAMA,CAAAA,CAAK,CAAG,SAAAC,CAAQ,CAAI,CAM7B,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 reset = 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"],"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 diff --git a/reportbuilder/amd/build/local/selectors.min.js b/reportbuilder/amd/build/local/selectors.min.js index ab4484959f5..a3255f4d00b 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:{systemReport:"[data-region=\"core_reportbuilder/system-report\"]",filterButtonLabel:"[data-region=\"filter-button-label\"]",filtersForm:"[data-region=\"filters-form\"]"},actions:{reportActionPopup:"[data-action=\"report-action-popup\"]",reportCreate:"[data-action=\"report-create\"]",reportEdit:"[data-action=\"report-edit\"]",reportDelete:"[data-action=\"report-delete\"]"}};b.forSystemReport=function(a){return"".concat(b.regions.systemReport,"[data-reportid=\"").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\"]"},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\"]",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 28c2daf951f..7c600e40107 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","systemReport","filterButtonLabel","filtersForm","actions","reportActionPopup","reportCreate","reportEdit","reportDelete","forSystemReport","reportId"],"mappings":"oJA+BA,GAAMA,CAAAA,CAAS,CAAG,CACdC,OAAO,CAAE,CACLC,YAAY,CAAE,oDADT,CAELC,iBAAiB,CAAE,uCAFd,CAGLC,WAAW,CAAE,gCAHR,CADK,CAMdC,OAAO,CAAE,CACLC,iBAAiB,CAAE,uCADd,CAELC,YAAY,CAAE,iCAFT,CAGLC,UAAU,CAAE,+BAHP,CAILC,YAAY,CAAE,iCAJT,CANK,CAAlB,CAqBAT,CAAS,CAACU,eAAV,CAA4B,SAAAC,CAAQ,kBAAOX,CAAS,CAACC,OAAV,CAAkBC,YAAzB,6BAAwDS,CAAxD,QAApC,C,UAEeX,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 systemReport: '[data-region=\"core_reportbuilder/system-report\"]',\n filterButtonLabel: '[data-region=\"filter-button-label\"]',\n filtersForm: '[data-region=\"filters-form\"]',\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 },\n};\n\n/**\n * Selector for given report\n *\n * @method forSystemReport\n * @param {Number} reportId\n * @return {String}\n */\nSELECTORS.forSystemReport = reportId => `${SELECTORS.regions.systemReport}[data-reportid=\"${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","actions","reportActionPopup","reportCreate","reportEdit","reportDelete","reportAddColumn","reportRemoveColumn","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,CADK,CAUdC,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,aAAa,CAAE,kCAPV,CAVK,CAAlB,CA4BAhB,CAAS,CAACiB,SAAV,CAAsB,SAAAC,CAAQ,kBAAOlB,CAAS,CAACC,OAAV,CAAkBC,MAAzB,8BAAmDgB,CAAnD,QAA9B,C,UAEelB,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 },\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 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/build/report.min.js b/reportbuilder/amd/build/report.min.js index 54a9849e106..212c8f03c3e 100644 --- a/reportbuilder/amd/build/report.min.js +++ b/reportbuilder/amd/build/report.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/report",["exports","core_reportbuilder/local/events","core_reportbuilder/local/selectors","core_table/dynamic","core_table/local/dynamic/selectors"],function(a,b,c,d,e){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=void 0;b=g(b);c=g(c);e=g(e);function f(){if("function"!=typeof WeakMap)return null;var a=new WeakMap;f=function(){return a};return a}function g(a){if(a&&a.__esModule){return a}if(null===a||"object"!==_typeof(a)&&"function"!=typeof a){return{default:a}}var b=f();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 g=d?Object.getOwnPropertyDescriptor(a,e):null;if(g&&(g.get||g.set)){Object.defineProperty(c,e,g)}else{c[e]=a[e]}}}c.default=a;if(b){b.set(a,c)}return c}function h(a,b,c,d,e,f,g){try{var h=a[f](g),i=h.value}catch(a){c(a);return}if(h.done){b(i)}else{Promise.resolve(i).then(d,e)}}function i(a){return function(){var b=this,c=arguments;return new Promise(function(d,e){var i=a.apply(b,c);function f(a){h(i,d,e,f,g,"next",a)}function g(a){h(i,d,e,f,g,"throw",a)}f(void 0)})}}var j=function(a){document.addEventListener(b.tableReload,function(){var b=i(regeneratorRuntime.mark(function b(f){var g,h,i,j;return regeneratorRuntime.wrap(function(b){while(1){switch(b.prev=b.next){case 0:h=f.target.closest(c.forSystemReport(a));if(!(null===h)){b.next=3;break}return b.abrupt("return");case 3:i=h.querySelector(e.main.region);j=(null===(g=f.detail)||void 0===g?void 0:g.preservePagination)?null:1;b.next=7;return(0,d.setPageNumber)(i,j,!1).then(d.refreshTableContent);case 7:case"end":return b.stop();}}},b)}));return function(){return b.apply(this,arguments)}}());document.addEventListener("click",function(a){var b=a.target.closest(c.actions.reportActionPopup);if(null===b){return}a.preventDefault();var d=JSON.parse(b.dataset.popupAction);window.openpopup(a,d.jsfunctionargs)})};a.init=j}); +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/report",["exports","core_reportbuilder/local/events","core_reportbuilder/local/selectors","core_table/dynamic","core_table/local/dynamic/selectors"],function(a,b,c,d,e){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=void 0;b=g(b);c=g(c);e=g(e);function f(){if("function"!=typeof WeakMap)return null;var a=new WeakMap;f=function(){return a};return a}function g(a){if(a&&a.__esModule){return a}if(null===a||"object"!==_typeof(a)&&"function"!=typeof a){return{default:a}}var b=f();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 g=d?Object.getOwnPropertyDescriptor(a,e):null;if(g&&(g.get||g.set)){Object.defineProperty(c,e,g)}else{c[e]=a[e]}}}c.default=a;if(b){b.set(a,c)}return c}function h(a,b,c,d,e,f,g){try{var h=a[f](g),i=h.value}catch(a){c(a);return}if(h.done){b(i)}else{Promise.resolve(i).then(d,e)}}function i(a){return function(){var b=this,c=arguments;return new Promise(function(d,e){var i=a.apply(b,c);function f(a){h(i,d,e,f,g,"next",a)}function g(a){h(i,d,e,f,g,"throw",a)}f(void 0)})}}var j=!1,k=function(){if(j){return}document.addEventListener(b.tableReload,function(){var a=i(regeneratorRuntime.mark(function a(b){var f,g,h,i;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:g=b.target.closest(c.regions.report);if(!(null===g)){a.next=3;break}return a.abrupt("return");case 3:h=g.querySelector(e.main.region);i=(null===(f=b.detail)||void 0===f?void 0:f.preservePagination)?null:1;a.next=7;return(0,d.setPageNumber)(h,i,!1).then(d.refreshTableContent);case 7:case"end":return a.stop();}}},a)}));return function(){return a.apply(this,arguments)}}());document.addEventListener("click",function(a){var b=a.target.closest(c.actions.reportActionPopup);if(null===b){return}a.preventDefault();var d=JSON.parse(b.dataset.popupAction);window.openpopup(a,d.jsfunctionargs)});j=!0};a.init=k}); //# sourceMappingURL=report.min.js.map diff --git a/reportbuilder/amd/build/report.min.js.map b/reportbuilder/amd/build/report.min.js.map index a9de2c83148..a59df83b8e2 100644 --- a/reportbuilder/amd/build/report.min.js.map +++ b/reportbuilder/amd/build/report.min.js.map @@ -1 +1 @@ -{"version":3,"sources":["../src/report.js"],"names":["init","reportId","document","addEventListener","reportEvents","tableReload","event","triggerElement","target","closest","reportSelectors","forSystemReport","tableRoot","querySelector","tableSelectors","main","region","pageNumber","detail","preservePagination","then","refreshTableContent","reportActionPopup","actions","preventDefault","popupAction","JSON","parse","dataset","window","openpopup","jsfunctionargs"],"mappings":"ojBAuBA,OACA,OAEA,O,q2BAQO,GAAMA,CAAAA,CAAI,CAAG,SAAAC,CAAQ,CAAI,CAE5BC,QAAQ,CAACC,gBAAT,CAA0BC,CAAY,CAACC,WAAvC,4CAAoD,WAAMC,CAAN,+FAC1CC,CAD0C,CACzBD,CAAK,CAACE,MAAN,CAAaC,OAAb,CAAqBC,CAAe,CAACC,eAAhB,CAAgCV,CAAhC,CAArB,CADyB,MAEzB,IAAnB,GAAAM,CAF4C,mDAM1CK,CAN0C,CAM9BL,CAAc,CAACM,aAAf,CAA6BC,CAAc,CAACC,IAAf,CAAoBC,MAAjD,CAN8B,CAO1CC,CAP0C,CAO7B,WAAAX,CAAK,CAACY,MAAN,uBAAcC,kBAAd,EAAmC,IAAnC,CAA0C,CAPb,gBAS1C,oBAAcP,CAAd,CAAyBK,CAAzB,KACDG,IADC,CACIC,qBADJ,CAT0C,yCAApD,yDAcAnB,QAAQ,CAACC,gBAAT,CAA0B,OAA1B,CAAmC,SAAAG,CAAK,CAAI,CACxC,GAAMgB,CAAAA,CAAiB,CAAGhB,CAAK,CAACE,MAAN,CAAaC,OAAb,CAAqBC,CAAe,CAACa,OAAhB,CAAwBD,iBAA7C,CAA1B,CACA,GAA0B,IAAtB,GAAAA,CAAJ,CAAgC,CAC5B,MACH,CACDhB,CAAK,CAACkB,cAAN,GACA,GAAMC,CAAAA,CAAW,CAAGC,IAAI,CAACC,KAAL,CAAWL,CAAiB,CAACM,OAAlB,CAA0BH,WAArC,CAApB,CACAI,MAAM,CAACC,SAAP,CAAiBxB,CAAjB,CAAwBmB,CAAW,CAACM,cAApC,CACH,CARD,CASH,CAzBM,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 report management\n *\n * @module core_reportbuilder/report\n * @copyright 2021 Paul Holden \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport * as reportEvents from 'core_reportbuilder/local/events';\nimport * as reportSelectors from 'core_reportbuilder/local/selectors';\nimport {setPageNumber, refreshTableContent} from 'core_table/dynamic';\nimport * as tableSelectors from 'core_table/local/dynamic/selectors';\n\n/**\n * Initialise module for given report\n *\n * @method\n * @param {Number} reportId\n */\nexport const init = reportId => {\n // Listen for the table reload event.\n document.addEventListener(reportEvents.tableReload, async(event) => {\n const triggerElement = event.target.closest(reportSelectors.forSystemReport(reportId));\n if (triggerElement === null) {\n return;\n }\n\n const tableRoot = triggerElement.querySelector(tableSelectors.main.region);\n const pageNumber = event.detail?.preservePagination ? null : 1;\n\n await setPageNumber(tableRoot, pageNumber, false)\n .then(refreshTableContent);\n });\n\n // Listen for trigger popup events.\n document.addEventListener('click', event => {\n const reportActionPopup = event.target.closest(reportSelectors.actions.reportActionPopup);\n if (reportActionPopup === null) {\n return;\n }\n event.preventDefault();\n const popupAction = JSON.parse(reportActionPopup.dataset.popupAction);\n window.openpopup(event, popupAction.jsfunctionargs);\n });\n};\n"],"file":"report.min.js"} \ No newline at end of file +{"version":3,"sources":["../src/report.js"],"names":["initialized","init","document","addEventListener","reportEvents","tableReload","event","reportElement","target","closest","reportSelectors","regions","report","tableRoot","querySelector","tableSelectors","main","region","pageNumber","detail","preservePagination","then","refreshTableContent","reportActionPopup","actions","preventDefault","popupAction","JSON","parse","dataset","window","openpopup","jsfunctionargs"],"mappings":"ojBAuBA,OACA,OAEA,O,w2BAEIA,CAAAA,CAAW,G,CAOFC,CAAI,CAAG,UAAM,CAEtB,GAAID,CAAJ,CAAiB,CAEb,MACH,CAGDE,QAAQ,CAACC,gBAAT,CAA0BC,CAAY,CAACC,WAAvC,4CAAoD,WAAMC,CAAN,+FAC1CC,CAD0C,CAC1BD,CAAK,CAACE,MAAN,CAAaC,OAAb,CAAqBC,CAAe,CAACC,OAAhB,CAAwBC,MAA7C,CAD0B,MAE1B,IAAlB,GAAAL,CAF4C,mDAM1CM,CAN0C,CAM9BN,CAAa,CAACO,aAAd,CAA4BC,CAAc,CAACC,IAAf,CAAoBC,MAAhD,CAN8B,CAO1CC,CAP0C,CAO7B,WAAAZ,CAAK,CAACa,MAAN,uBAAcC,kBAAd,EAAmC,IAAnC,CAA0C,CAPb,gBAS1C,oBAAcP,CAAd,CAAyBK,CAAzB,KACDG,IADC,CACIC,qBADJ,CAT0C,yCAApD,yDAcApB,QAAQ,CAACC,gBAAT,CAA0B,OAA1B,CAAmC,SAAAG,CAAK,CAAI,CACxC,GAAMiB,CAAAA,CAAiB,CAAGjB,CAAK,CAACE,MAAN,CAAaC,OAAb,CAAqBC,CAAe,CAACc,OAAhB,CAAwBD,iBAA7C,CAA1B,CACA,GAA0B,IAAtB,GAAAA,CAAJ,CAAgC,CAC5B,MACH,CACDjB,CAAK,CAACmB,cAAN,GACA,GAAMC,CAAAA,CAAW,CAAGC,IAAI,CAACC,KAAL,CAAWL,CAAiB,CAACM,OAAlB,CAA0BH,WAArC,CAApB,CACAI,MAAM,CAACC,SAAP,CAAiBzB,CAAjB,CAAwBoB,CAAW,CAACM,cAApC,CACH,CARD,EAUAhC,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 report management\n *\n * @module core_reportbuilder/report\n * @copyright 2021 Paul Holden \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport * as reportEvents from 'core_reportbuilder/local/events';\nimport * as reportSelectors from 'core_reportbuilder/local/selectors';\nimport {setPageNumber, refreshTableContent} from 'core_table/dynamic';\nimport * as tableSelectors from 'core_table/local/dynamic/selectors';\n\nlet initialized = false;\n\n/**\n * Initialise module for given report\n *\n * @method\n */\nexport const init = () => {\n\n if (initialized) {\n // We already added the event listeners (can be called multiple times by mustache template).\n return;\n }\n\n // Listen for the table reload event.\n document.addEventListener(reportEvents.tableReload, async(event) => {\n const reportElement = event.target.closest(reportSelectors.regions.report);\n if (reportElement === null) {\n return;\n }\n\n const tableRoot = reportElement.querySelector(tableSelectors.main.region);\n const pageNumber = event.detail?.preservePagination ? null : 1;\n\n await setPageNumber(tableRoot, pageNumber, false)\n .then(refreshTableContent);\n });\n\n // Listen for trigger popup events.\n document.addEventListener('click', event => {\n const reportActionPopup = event.target.closest(reportSelectors.actions.reportActionPopup);\n if (reportActionPopup === null) {\n return;\n }\n event.preventDefault();\n const popupAction = JSON.parse(reportActionPopup.dataset.popupAction);\n window.openpopup(event, popupAction.jsfunctionargs);\n });\n\n initialized = true;\n};\n"],"file":"report.min.js"} \ No newline at end of file diff --git a/reportbuilder/amd/build/sidebar.min.js b/reportbuilder/amd/build/sidebar.min.js new file mode 100644 index 00000000000..de16bc9401d --- /dev/null +++ b/reportbuilder/amd/build/sidebar.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/sidebar",["exports","core/pending","core/utils","core_reportbuilder/local/selectors"],function(a,b,c,d){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=void 0;b=function(a){return a&&a.__esModule?a:{default:a}}(b);d=f(d);function e(){if("function"!=typeof WeakMap)return null;var a=new WeakMap;e=function(){return a};return a}function f(a){if(a&&a.__esModule){return a}if(null===a||"object"!==_typeof(a)&&"function"!=typeof a){return{default:a}}var b=e();if(b&&b.has(a)){return b.get(a)}var c={},d=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var f in a){if(Object.prototype.hasOwnProperty.call(a,f)){var g=d?Object.getOwnPropertyDescriptor(a,f):null;if(g&&(g.get||g.set)){Object.defineProperty(c,f,g)}else{c[f]=a[f]}}}c.default=a;if(b){b.set(a,c)}return c}var g={EXPANDED:"show",COLLAPSED:"collapsed",SHOW:"d-flex",HIDE:"d-none"},h=function(a,c){var e=new b.default("core_reportbuilder/sidebar:cardFilter"),f=c.querySelectorAll(d.regions.sidebarCard),h=c.querySelectorAll(".list-group-item"),j=a.target.value.toLowerCase();h.forEach(function(a){var b=a.dataset.name.toLowerCase();if(b.includes(j)){a.classList.replace(g.HIDE,g.SHOW)}else{a.classList.replace(g.SHOW,g.HIDE)}});f.forEach(function(a){if(0.\n\n/**\n * Report builder sidebar component\n *\n * @module core_reportbuilder/sidebar\n * @package core_reportbuilder\n * @copyright 2021 Paul Holden \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Pending from 'core/pending';\nimport {debounce} from 'core/utils';\nimport * as reportSelectors from 'core_reportbuilder/local/selectors';\n\nconst DEBOUNCE_TIMER = 250;\n\nconst CLASSES = {\n EXPANDED: 'show',\n COLLAPSED: 'collapsed',\n SHOW: 'd-flex',\n HIDE: 'd-none',\n};\n\n/**\n * Initialise module\n *\n * @param {Event} event\n * @param {Element} sidebarMenu\n */\nconst sidebarCardFilter = (event, sidebarMenu) => {\n const pendingPromise = new Pending('core_reportbuilder/sidebar:cardFilter');\n\n const sidebarCards = sidebarMenu.querySelectorAll(reportSelectors.regions.sidebarCard);\n const sidebarItems = sidebarMenu.querySelectorAll('.list-group-item');\n const searchTerm = event.target.value.toLowerCase();\n\n // Toggle items according to match against search term.\n sidebarItems.forEach(item => {\n let itemName = item.dataset.name.toLowerCase();\n if (itemName.includes(searchTerm)) {\n item.classList.replace(CLASSES.HIDE, CLASSES.SHOW);\n } else {\n item.classList.replace(CLASSES.SHOW, CLASSES.HIDE);\n }\n });\n\n // Toggle cards according to whether they have any visible items.\n sidebarCards.forEach(card => {\n if (card.querySelectorAll(`.${CLASSES.SHOW}`).length > 0) {\n card.classList.remove(CLASSES.HIDE);\n expandCard(card);\n } else {\n card.classList.add(CLASSES.HIDE);\n }\n });\n\n pendingPromise.resolve();\n};\n\n/**\n * Show a collapsed card.\n * This function simulates the behaviour of JQuery show method on a collapsible element.\n *\n * @param {Element} card\n */\nconst expandCard = (card) => {\n let cardButton = card.querySelector('[data-toggle=\"collapse\"]');\n if (cardButton.classList.contains(CLASSES.COLLAPSED)) {\n cardButton.classList.remove(CLASSES.COLLAPSED);\n cardButton.setAttribute('aria-expanded', \"true\");\n let cardContent = card.querySelector(cardButton.dataset.target);\n cardContent.classList.add(CLASSES.EXPANDED);\n }\n};\n\n/**\n * Initialise module\n */\nexport const init = () => {\n const sidebarMenu = document.querySelector(reportSelectors.regions.sidebarMenu);\n const sidebarSearch = sidebarMenu.querySelector(reportSelectors.actions.sidebarSearch);\n\n // Debounce the event listener to allow the user to finish typing.\n sidebarSearch.addEventListener('keyup', event => {\n const pendingPromise = new Pending('core_reportbuilder/sidebar:keyup');\n\n debounce(sidebarCardFilter, DEBOUNCE_TIMER)(event, sidebarMenu);\n setTimeout(() => {\n pendingPromise.resolve();\n }, DEBOUNCE_TIMER);\n });\n};\n"],"file":"sidebar.min.js"} \ No newline at end of file diff --git a/reportbuilder/amd/src/editor.js b/reportbuilder/amd/src/editor.js new file mode 100644 index 00000000000..d246bb12c60 --- /dev/null +++ b/reportbuilder/amd/src/editor.js @@ -0,0 +1,77 @@ +// 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 editor + * + * @module core_reportbuilder/editor + * @package core_reportbuilder + * @copyright 2021 David Matamoros + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +"use strict"; + +import 'core/inplace_editable'; +import Notification from 'core/notification'; +import Pending from 'core/pending'; +import Templates from 'core/templates'; +import * as reportSelectors from 'core_reportbuilder/local/selectors'; +import {init as columnsEditorInit} from 'core_reportbuilder/local/editor/columns'; +import {getReport} from 'core_reportbuilder/local/repository/reports'; + +let initialized = false; + +/** + * Initialise editor and all it's modules + */ +export const init = () => { + const reportElement = document.querySelector(reportSelectors.regions.report); + + columnsEditorInit(reportElement, initialized); + + // Ensure we only add our listeners once (can be called multiple times by mustache template). + if (initialized) { + return; + } + + // Add event handlers to generic report editor elements. + document.addEventListener('click', event => { + + // Toggle between edit and preview mode. + const toggleEditViewMode = event.target.closest(reportSelectors.actions.toggleEditPreview); + if (toggleEditViewMode) { + event.preventDefault(); + + const pendingPromise = new Pending('core_reportbuilder/reports:get'); + const toggledEditMode = toggleEditViewMode.dataset.editMode !== "1"; + + let customjs = ''; + + getReport(reportElement.dataset.reportId, toggledEditMode) + .then(response => { + customjs = response.javascript; + return Templates.render('core_reportbuilder/custom_report', response); + }) + .then((html, js) => { + return Templates.replaceNodeContents(reportElement, html, js + customjs); + }) + .then(() => pendingPromise.resolve()) + .catch(Notification.exception); + } + }); + + initialized = true; +}; diff --git a/reportbuilder/amd/src/filters.js b/reportbuilder/amd/src/filters.js index dffb4467ea1..25c4c54af47 100644 --- a/reportbuilder/amd/src/filters.js +++ b/reportbuilder/amd/src/filters.js @@ -31,7 +31,7 @@ import {add as addToast} from 'core/toast'; import DynamicForm from 'core_form/dynamicform'; import * as reportEvents from 'core_reportbuilder/local/events'; import * as reportSelectors from 'core_reportbuilder/local/selectors'; -import {reset as resetFilters} from 'core_reportbuilder/local/repository/filters'; +import {resetFilters} from 'core_reportbuilder/local/repository/filters'; /** * Update filter button text to indicate applied filter count @@ -57,7 +57,7 @@ const setFilterButtonCount = async(reportElement, filterCount) => { * @param {Number} contextId */ export const init = (reportId, contextId) => { - const reportElement = document.querySelector(reportSelectors.forSystemReport(reportId)); + const reportElement = document.querySelector(reportSelectors.forReport(reportId)); const filterFormContainer = reportElement.querySelector(reportSelectors.regions.filtersForm); const filterForm = new DynamicForm(filterFormContainer, '\\core_reportbuilder\\form\\filter'); diff --git a/reportbuilder/amd/src/local/editor/columns.js b/reportbuilder/amd/src/local/editor/columns.js new file mode 100644 index 00000000000..d51e395cd95 --- /dev/null +++ b/reportbuilder/amd/src/local/editor/columns.js @@ -0,0 +1,145 @@ +// 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 columns editor + * + * @module core_reportbuilder/local/editor/columns + * @package core_reportbuilder + * @copyright 2021 Paul Holden + * @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 {publish} from 'core/pubsub'; +import SortableList from 'core/sortable_list'; +import {get_string as getString, get_strings as getStrings} from 'core/str'; +import {add as addToast} from 'core/toast'; +import * as reportEvents from 'core_reportbuilder/local/events'; +import * as reportSelectors from 'core_reportbuilder/local/selectors'; +import {addColumn, deleteColumn, reorderColumn} from 'core_reportbuilder/local/repository/columns'; + +/** + * 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 column to report. + const reportAddColumn = event.target.closest(reportSelectors.actions.reportAddColumn); + if (reportAddColumn) { + event.preventDefault(); + + const pendingPromise = new Pending('core_reportbuilder/columns:add'); + + addColumn(reportElement.dataset.reportId, reportAddColumn.dataset.uniqueIdentifier) + .then(data => publish(reportEvents.publish.reportColumnsUpdated, data)) + .then(() => getString('columnadded', 'core_reportbuilder', reportAddColumn.dataset.name)) + .then(addToast) + .then(() => { + dispatchEvent(reportEvents.tableReload, {preservePagination: true}, reportElement); + return pendingPromise.resolve(); + }) + .catch(Notification.exception); + } + + // Remove column from report. + const reportRemoveColumn = event.target.closest(reportSelectors.actions.reportRemoveColumn); + if (reportRemoveColumn) { + event.preventDefault(); + + const columnHeader = reportRemoveColumn.closest(reportSelectors.regions.columnHeader); + const columnName = columnHeader.dataset.columnName; + + getStrings([ + {key: 'deletecolumn', component: 'core_reportbuilder', param: columnName}, + {key: 'deletecolumnconfirm', component: 'core_reportbuilder', param: columnName}, + {key: 'delete', component: 'moodle'}, + ]).then(([confirmTitle, confirmText, confirmButton]) => { + Notification.confirm(confirmTitle, confirmText, confirmButton, null, () => { + const pendingPromise = new Pending('core_reportbuilder/columns:remove'); + + deleteColumn(reportElement.dataset.reportId, columnHeader.dataset.columnId) + .then(data => publish(reportEvents.publish.reportColumnsUpdated, data)) + .then(() => getString('columndeleted', 'core_reportbuilder', columnName)) + .then(addToast) + .then(() => { + dispatchEvent(reportEvents.tableReload, {preservePagination: true}, reportElement); + return pendingPromise.resolve(); + }) + .catch(Notification.exception); + }); + return; + }).catch(Notification.exception); + } + }); + + // Initialize sortable list to handle column moving (note JQuery dependency, see MDL-72293 for resolution). + var columnSortableList = new SortableList(`${reportSelectors.regions.reportTable} thead tr`, {isHorizontal: true}); + columnSortableList.getElementName = element => Promise.resolve(element.data('columnName')); + + $(reportElement).on(SortableList.EVENTS.DRAG, 'th[data-column-id]', (event, info) => { + const columnPosition = info.element.data('columnPosition'); + const targetColumnPosition = info.targetNextElement.data('columnPosition'); + + $(reportElement).find('tbody tr').each(function() { + const cell = $(this).children(`td.c${columnPosition - 1}`)[0]; + if (targetColumnPosition) { + var beforeCell = $(this).children(`td.c${targetColumnPosition - 1}`)[0]; + this.insertBefore(cell, beforeCell); + } else { + this.appendChild(cell); + } + }); + }); + + $(reportElement).on(SortableList.EVENTS.DROP, 'th[data-column-id]', (event, info) => { + if (info.positionChanged) { + const pendingPromise = new Pending('core_reportbuilder/columns:reorder'); + + const columnId = info.element.data('columnId'); + const columnName = info.element.data('columnName'); + const columnPosition = info.element.data('columnPosition'); + + // Select target position, if moving to the end then count number of element siblings. + let targetColumnPosition = info.targetNextElement.data('columnPosition') || info.element.siblings().length + 2; + if (targetColumnPosition > columnPosition) { + targetColumnPosition--; + } + + reorderColumn(reportElement.dataset.reportId, columnId, targetColumnPosition) + .then(() => getString('columnmoved', 'core_reportbuilder', columnName)) + .then(addToast) + .then(() => { + dispatchEvent(reportEvents.tableReload, {preservePagination: true}, reportElement); + return pendingPromise.resolve(); + }) + .catch(Notification.exception); + } + }); +}; diff --git a/reportbuilder/amd/src/local/repository/columns.js b/reportbuilder/amd/src/local/repository/columns.js new file mode 100644 index 00000000000..0804e25b251 --- /dev/null +++ b/reportbuilder/amd/src/local/repository/columns.js @@ -0,0 +1,74 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Module to handle column AJAX requests + * + * @module core_reportbuilder/local/repository/columns + * @package core_reportbuilder + * @copyright 2021 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import Ajax from 'core/ajax'; + +/** + * Add column to given report + * + * @param {Number} reportId + * @param {String} uniqueIdentifier + * @return {Promise} + */ +export const addColumn = (reportId, uniqueIdentifier) => { + const request = { + methodname: 'core_reportbuilder_columns_add', + args: {reportid: reportId, uniqueidentifier: uniqueIdentifier} + }; + + return Ajax.call([request])[0]; +}; + +/** + * Remove column from given report + * + * @param {Number} reportId + * @param {Number} columnId + * @return {Promise} + */ +export const deleteColumn = (reportId, columnId) => { + const request = { + methodname: 'core_reportbuilder_columns_delete', + args: {reportid: reportId, columnid: columnId} + }; + + return Ajax.call([request])[0]; +}; + +/** + * Re-order column within a report + * + * @param {Number} reportId + * @param {Number} columnId + * @param {Number} position + * @return {Promise} + */ +export const reorderColumn = (reportId, columnId, position) => { + const request = { + methodname: 'core_reportbuilder_columns_reorder', + args: {reportid: reportId, columnid: columnId, position: position} + }; + + return Ajax.call([request])[0]; +}; diff --git a/reportbuilder/amd/src/local/repository/filters.js b/reportbuilder/amd/src/local/repository/filters.js index 3a82d9d6b7b..02c580fda71 100644 --- a/reportbuilder/amd/src/local/repository/filters.js +++ b/reportbuilder/amd/src/local/repository/filters.js @@ -30,7 +30,7 @@ import Ajax from 'core/ajax'; * @param {Number} reportId * @return {Promise} */ -export const reset = reportId => { +export const resetFilters = reportId => { const request = { methodname: 'core_reportbuilder_filters_reset', args: {reportid: reportId} diff --git a/reportbuilder/amd/src/local/selectors.js b/reportbuilder/amd/src/local/selectors.js index aa44777bbe4..bfadfd0dfb2 100644 --- a/reportbuilder/amd/src/local/selectors.js +++ b/reportbuilder/amd/src/local/selectors.js @@ -31,25 +31,32 @@ */ const SELECTORS = { regions: { - systemReport: '[data-region="core_reportbuilder/system-report"]', + 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"]', }, 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"]', + sidebarSearch: '[data-action="sidebar-search"]', }, }; /** * Selector for given report * - * @method forSystemReport + * @method forReport * @param {Number} reportId * @return {String} */ -SELECTORS.forSystemReport = reportId => `${SELECTORS.regions.systemReport}[data-reportid="${reportId}"]`; +SELECTORS.forReport = reportId => `${SELECTORS.regions.report}[data-report-id="${reportId}"]`; export default SELECTORS; diff --git a/reportbuilder/amd/src/report.js b/reportbuilder/amd/src/report.js index 12822235785..98d24878310 100644 --- a/reportbuilder/amd/src/report.js +++ b/reportbuilder/amd/src/report.js @@ -26,21 +26,28 @@ import * as reportSelectors from 'core_reportbuilder/local/selectors'; import {setPageNumber, refreshTableContent} from 'core_table/dynamic'; import * as tableSelectors from 'core_table/local/dynamic/selectors'; +let initialized = false; + /** * Initialise module for given report * * @method - * @param {Number} reportId */ -export const init = reportId => { +export const init = () => { + + if (initialized) { + // We already added the event listeners (can be called multiple times by mustache template). + return; + } + // Listen for the table reload event. document.addEventListener(reportEvents.tableReload, async(event) => { - const triggerElement = event.target.closest(reportSelectors.forSystemReport(reportId)); - if (triggerElement === null) { + const reportElement = event.target.closest(reportSelectors.regions.report); + if (reportElement === null) { return; } - const tableRoot = triggerElement.querySelector(tableSelectors.main.region); + const tableRoot = reportElement.querySelector(tableSelectors.main.region); const pageNumber = event.detail?.preservePagination ? null : 1; await setPageNumber(tableRoot, pageNumber, false) @@ -57,4 +64,6 @@ export const init = reportId => { const popupAction = JSON.parse(reportActionPopup.dataset.popupAction); window.openpopup(event, popupAction.jsfunctionargs); }); + + initialized = true; }; diff --git a/reportbuilder/amd/src/sidebar.js b/reportbuilder/amd/src/sidebar.js new file mode 100644 index 00000000000..9324b5c04c0 --- /dev/null +++ b/reportbuilder/amd/src/sidebar.js @@ -0,0 +1,106 @@ +// 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 sidebar component + * + * @module core_reportbuilder/sidebar + * @package core_reportbuilder + * @copyright 2021 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import Pending from 'core/pending'; +import {debounce} from 'core/utils'; +import * as reportSelectors from 'core_reportbuilder/local/selectors'; + +const DEBOUNCE_TIMER = 250; + +const CLASSES = { + EXPANDED: 'show', + COLLAPSED: 'collapsed', + SHOW: 'd-flex', + HIDE: 'd-none', +}; + +/** + * Initialise module + * + * @param {Event} event + * @param {Element} sidebarMenu + */ +const sidebarCardFilter = (event, sidebarMenu) => { + const pendingPromise = new Pending('core_reportbuilder/sidebar:cardFilter'); + + const sidebarCards = sidebarMenu.querySelectorAll(reportSelectors.regions.sidebarCard); + const sidebarItems = sidebarMenu.querySelectorAll('.list-group-item'); + const searchTerm = event.target.value.toLowerCase(); + + // Toggle items according to match against search term. + sidebarItems.forEach(item => { + let itemName = item.dataset.name.toLowerCase(); + if (itemName.includes(searchTerm)) { + item.classList.replace(CLASSES.HIDE, CLASSES.SHOW); + } else { + item.classList.replace(CLASSES.SHOW, CLASSES.HIDE); + } + }); + + // Toggle cards according to whether they have any visible items. + sidebarCards.forEach(card => { + if (card.querySelectorAll(`.${CLASSES.SHOW}`).length > 0) { + card.classList.remove(CLASSES.HIDE); + expandCard(card); + } else { + card.classList.add(CLASSES.HIDE); + } + }); + + pendingPromise.resolve(); +}; + +/** + * Show a collapsed card. + * This function simulates the behaviour of JQuery show method on a collapsible element. + * + * @param {Element} card + */ +const expandCard = (card) => { + let cardButton = card.querySelector('[data-toggle="collapse"]'); + if (cardButton.classList.contains(CLASSES.COLLAPSED)) { + cardButton.classList.remove(CLASSES.COLLAPSED); + cardButton.setAttribute('aria-expanded', "true"); + let cardContent = card.querySelector(cardButton.dataset.target); + cardContent.classList.add(CLASSES.EXPANDED); + } +}; + +/** + * Initialise module + */ +export const init = () => { + const sidebarMenu = document.querySelector(reportSelectors.regions.sidebarMenu); + const sidebarSearch = sidebarMenu.querySelector(reportSelectors.actions.sidebarSearch); + + // Debounce the event listener to allow the user to finish typing. + sidebarSearch.addEventListener('keyup', event => { + const pendingPromise = new Pending('core_reportbuilder/sidebar:keyup'); + + debounce(sidebarCardFilter, DEBOUNCE_TIMER)(event, sidebarMenu); + setTimeout(() => { + pendingPromise.resolve(); + }, DEBOUNCE_TIMER); + }); +}; diff --git a/reportbuilder/classes/datasource.php b/reportbuilder/classes/datasource.php index f257920536a..d5c089e40f6 100644 --- a/reportbuilder/classes/datasource.php +++ b/reportbuilder/classes/datasource.php @@ -40,4 +40,58 @@ abstract class datasource extends base { * @return string */ abstract public static function get_name(): string; + + /** + * Add columns from the given entity name to be available to use in a custom report + * + * @param string $entityname + * @param array $include Include only these columns, if omitted then include all + * @param array $exclude Exclude these columns, if omitted then exclude none + * @throws coding_exception If both $include and $exclude are non-empty + */ + final protected function add_columns_from_entity(string $entityname, array $include = [], array $exclude = []): void { + if (!empty($include) && !empty($exclude)) { + throw new coding_exception('Cannot specify columns to include and exclude simultaneously'); + } + + $entity = $this->get_entity($entityname); + + // Retrieve filtered columns from entity, respecting given $include/$exclude parameters. + $columns = array_filter($entity->get_columns(), static function(column $column) use ($include, $exclude): bool { + if (!empty($include)) { + return in_array($column->get_name(), $include); + } + + if (!empty($exclude)) { + return !in_array($column->get_name(), $exclude); + } + + return true; + }); + + foreach ($columns as $column) { + $this->add_column($column); + } + } + + /** + * Add default datasource columns to the report + * + * This method is optional and can be called when the report is created to add the default columns defined in the + * selected datasource. + */ + public function add_default_columns(): void { + $reportid = $this->get_report_persistent()->get('id'); + $columnidentifiers = $this->get_default_columns(); + foreach ($columnidentifiers as $uniqueidentifier) { + report::add_report_column($reportid, $uniqueidentifier); + } + } + + /** + * Return the columns that will be added to the report once is created + * + * @return string[] + */ + abstract public function get_default_columns(): array; } diff --git a/reportbuilder/classes/external/columns/add.php b/reportbuilder/classes/external/columns/add.php new file mode 100644 index 00000000000..f79b163c24c --- /dev/null +++ b/reportbuilder/classes/external/columns/add.php @@ -0,0 +1,96 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\external\columns; + +use core_reportbuilder\external\custom_report_columns_sorting_exporter; +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\local\helpers\report; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once("{$CFG->libdir}/externallib.php"); + +/** + * External method for adding report columns + * + * @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 column'), + ]); + } + + /** + * 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_column($reportid, $uniqueidentifier); + + $exporter = new custom_report_columns_sorting_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_columns_sorting_exporter::get_read_structure(); + } +} diff --git a/reportbuilder/classes/external/columns/delete.php b/reportbuilder/classes/external/columns/delete.php new file mode 100644 index 00000000000..270f177866b --- /dev/null +++ b/reportbuilder/classes/external/columns/delete.php @@ -0,0 +1,96 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\external\columns; + +use core_reportbuilder\external\custom_report_columns_sorting_exporter; +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\local\helpers\report; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once("{$CFG->libdir}/externallib.php"); + +/** + * External method for deleting report columns + * + * @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'), + 'columnid' => new external_value(PARAM_INT, 'Column ID'), + ]); + } + + /** + * External method execution + * + * @param int $reportid + * @param int $columnid + * @return array + */ + public static function execute(int $reportid, int $columnid): array { + global $PAGE; + + [ + 'reportid' => $reportid, + 'columnid' => $columnid, + ] = self::validate_parameters(self::execute_parameters(), [ + 'reportid' => $reportid, + 'columnid' => $columnid, + ]); + + $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_column($reportid, $columnid); + + $exporter = new custom_report_columns_sorting_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_columns_sorting_exporter::get_read_structure(); + } +} diff --git a/reportbuilder/classes/external/columns/reorder.php b/reportbuilder/classes/external/columns/reorder.php new file mode 100644 index 00000000000..d2624fc2744 --- /dev/null +++ b/reportbuilder/classes/external/columns/reorder.php @@ -0,0 +1,90 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\external\columns; + +use external_api; +use external_function_parameters; +use external_value; +use core_reportbuilder\manager; +use core_reportbuilder\permission; +use core_reportbuilder\local\helpers\report; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once("{$CFG->libdir}/externallib.php"); + +/** + * External method for re-ordering report columns + * + * @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'), + 'columnid' => new external_value(PARAM_INT, 'Column ID'), + 'position' => new external_value(PARAM_INT, 'New column position') + ]); + } + + /** + * External method execution + * + * @param int $reportid + * @param int $columnid + * @param int $position + * @return bool + */ + public static function execute(int $reportid, int $columnid, int $position): bool { + [ + 'reportid' => $reportid, + 'columnid' => $columnid, + 'position' => $position, + ] = self::validate_parameters(self::execute_parameters(), [ + 'reportid' => $reportid, + 'columnid' => $columnid, + 'position' => $position, + ]); + + $report = manager::get_report_from_id($reportid); + + self::validate_context($report->get_context()); + permission::require_can_edit_report($report->get_report_persistent()); + + return report::reorder_report_column($reportid, $columnid, $position); + } + + /** + * External method return value + * + * @return external_value + */ + public static function execute_returns(): external_value { + return new external_value(PARAM_BOOL, 'Success'); + } +} diff --git a/reportbuilder/classes/external/custom_report_columns_sorting_exporter.php b/reportbuilder/classes/external/custom_report_columns_sorting_exporter.php new file mode 100644 index 00000000000..e2b8b96f753 --- /dev/null +++ b/reportbuilder/classes/external/custom_report_columns_sorting_exporter.php @@ -0,0 +1,131 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\external; + +use pix_icon; +use renderer_base; +use core\external\exporter; +use core_reportbuilder\local\models\column; +use core_reportbuilder\local\report\base; + +/** + * Custom report columns sorting exporter class + * + * @package core_reportbuilder + * @copyright 2021 David Matamoros + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class custom_report_columns_sorting_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 [ + 'hassortablecolumns' => [ + 'type' => PARAM_BOOL, + ], + 'sortablecolumns' => [ + 'type' => [ + 'id' => ['type' => PARAM_INT], + 'title' => ['type' => PARAM_TEXT], + 'heading' => ['type' => PARAM_TEXT], + 'sortdirection' => ['type' => PARAM_INT], + 'sortenabled' => ['type' => PARAM_INT], + 'sortorder' => ['type' => PARAM_INT], + 'sorticon' => [ + 'type' => [ + 'key' => ['type' => PARAM_RAW], + 'component' => ['type' => PARAM_COMPONENT], + 'title' => ['type' => PARAM_NOTAGS], + ], + ], + 'movetitle' => ['type' => PARAM_TEXT], + 'sortenabledtitle' => ['type' => PARAM_TEXT], + ], + 'optional' => true, + 'multiple' => 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']; + + $reportid = $report->get_report_persistent()->get('id'); + $activecolumns = column::get_records(['reportid' => $reportid], 'sortorder'); + $sortablecolumns = array_filter($activecolumns, function(column $persistent) use($report) { + $column = $report->get_column($persistent->get('uniqueidentifier')); + return $column && $column->get_is_sortable(); + }); + + $sortablecolumns = array_map(function(column $persistent) use ($report) { + $columntitle = $report->get_column($persistent->get('uniqueidentifier'))->get_title(); + $columnheading = $persistent->get('heading') ?: $columntitle; + + $columnsortascending = ($persistent->get('sortdirection') == SORT_ASC); + $sortenabledtitle = $persistent->get('sortenabled') ? 'columnsortdisable' : 'columnsortenable'; + $sortdirectiontitle = $columnsortascending ? 'columnsortdirectiondesc' : 'columnsortdirectionasc'; + + $icon = $columnsortascending ? 't/uplong' : 't/downlong'; + $sorticon = new pix_icon($icon, get_string($sortdirectiontitle, 'core_reportbuilder', $columntitle)); + + return [ + 'id' => $persistent->get('id'), + 'title' => $columntitle, + 'heading' => $columnheading, + 'sortdirection' => $persistent->get('sortdirection'), + 'sortenabled' => (int)$persistent->get('sortenabled'), + 'sortorder' => $persistent->get('sortorder'), + 'sorticon' => $sorticon->export_for_pix(), + 'movetitle' => get_string('movesorting', 'core_reportbuilder', $columntitle), + 'sortenabledtitle' => get_string($sortenabledtitle, 'core_reportbuilder', $columntitle), + ]; + }, $sortablecolumns); + + return [ + 'hassortablecolumns' => !empty($sortablecolumns), + 'sortablecolumns' => array_values($sortablecolumns), + 'helpicon' => $output->help_icon('sorting', 'core_reportbuilder'), + ]; + } +} diff --git a/reportbuilder/classes/external/custom_report_exporter.php b/reportbuilder/classes/external/custom_report_exporter.php new file mode 100644 index 00000000000..9a6d5b3f800 --- /dev/null +++ b/reportbuilder/classes/external/custom_report_exporter.php @@ -0,0 +1,151 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\external; + +use renderer_base; +use core\persistent; +use core\external\persistent_exporter; +use core_reportbuilder\manager; +use core_reportbuilder\datasource; +use core_reportbuilder\form\filter as form_filter; +use core_reportbuilder\local\models\report; +use core_reportbuilder\table\custom_report_table; +use core_reportbuilder\table\custom_report_table_filterset; +use core_reportbuilder\table\custom_report_table_view; +use core_reportbuilder\table\custom_report_table_view_filterset; +use core_reportbuilder\local\helpers\report as report_helper; + +/** + * Custom report exporter class + * + * @package core_reportbuilder + * @copyright 2021 David Matamoros + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class custom_report_exporter extends persistent_exporter { + + /** @var report The persistent object we will export. */ + protected $persistent = null; + + /** @var bool */ + protected $editmode; + + /** @var bool $showeditbutton When showing the report on view.php the Edit button has to be hidden */ + protected $showeditbutton; + + /** + * report_exporter constructor. + * + * @param persistent $persistent + * @param array $related + */ + public function __construct(persistent $persistent, array $related = array(), bool $editmode = true, bool $showeditbutton = true) { + parent::__construct($persistent, $related); + $this->editmode = $editmode; + $this->showeditbutton = $showeditbutton; + } + /** + * Return the name of the class we are exporting + * + * @return string + */ + protected static function define_class(): string { + return report::class; + } + + /** + * Return a list of objects that are related to the persistent + * + * @return array + */ + protected static function define_related(): array { + return [ + ]; + } + + /** + * Return a list of additional properties used only for display + * + * @return array + */ + protected static function define_other_properties(): array { + return [ + 'table' => ['type' => PARAM_RAW], + 'sidebarmenucards' => ['type' => custom_report_menu_cards_exporter::read_properties_definition()], + 'filtersform' => [ + 'type' => PARAM_RAW, + 'optional' => true, + ], + 'editmode' => ['type' => PARAM_INT], + 'showeditbutton' => ['type' => PARAM_BOOL], + 'javascript' => ['type' => PARAM_RAW], + ]; + } + + /** + * Get additional values to inject while exporting + * + * @param renderer_base $output + * @return array + */ + protected function get_other_values(renderer_base $output): array { + $filtersform = ''; + $menucards = []; + + if ($this->editmode) { + $table = custom_report_table::create($this->persistent->get('id')); + $table->set_filterset(new custom_report_table_filterset()); + } else { + + } + + $report = manager::get_report_from_persistent($this->persistent); + + if ($this->editmode) { + $menucardexporter = new custom_report_menu_cards_exporter(null, [ + 'menucards' => report_helper::get_available_columns($report->get_report_persistent()) + ]); + $menucards = (array) $menucardexporter->export($output); + } + + return [ + 'table' => $output->render($table), + 'sidebarmenucards' => $menucards, + 'filtersform' => $filtersform, + 'editmode' => (int)$this->editmode, + 'showeditbutton' => $this->showeditbutton, + 'javascript' => '', + ]; + } + + /** + * Generate filters form for the report + * + * @return form_filter + */ + private function generate_filters_form(): form_filter { + $filtersform = new form_filter(null, null, 'post', '', [], true, [ + 'reportid' => $this->persistent->get('id'), + 'parameters' => json_encode([]), + ]); + $filtersform->set_data_for_dynamic_submission(); + + return $filtersform; + } +} diff --git a/reportbuilder/classes/external/custom_report_menu_cards_exporter.php b/reportbuilder/classes/external/custom_report_menu_cards_exporter.php new file mode 100644 index 00000000000..d46b1e5890a --- /dev/null +++ b/reportbuilder/classes/external/custom_report_menu_cards_exporter.php @@ -0,0 +1,97 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\external; + +use renderer_base; +use core\external\exporter; + +/** + * Custom report menu cards exporter class + * + * @package core_reportbuilder + * @copyright 2021 David Matamoros + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class custom_report_menu_cards_exporter extends exporter { + + /** + * Return a list of objects that are related to the exporter + * + * @return array + */ + protected static function define_related(): array { + return [ + 'menucards' => 'array[]', + ]; + } + + /** + * Return the list of additional properties for read structure and export + * + * @return array[] + */ + protected static function define_other_properties(): array { + return [ + 'menucards' => [ + 'type' => [ + 'name' => [ + 'type' => PARAM_TEXT, + 'optional' => true, + ], + 'key' => [ + 'type' => PARAM_TEXT, + 'optional' => true, + ], + 'items' => [ + 'type' => [ + 'name' => [ + 'type' => PARAM_TEXT, + ], + 'identifier' => [ + 'type' => PARAM_TEXT, + ], + 'title' => [ + 'type' => PARAM_TEXT, + ], + 'action' => [ + 'type' => PARAM_TEXT, + ], + ], + 'optional' => true, + 'multiple' => true, + ], + ], + 'optional' => true, + 'multiple' => true, + ], + ]; + } + + /** + * Get the additional values to inject while exporting + * + * @param renderer_base $output + * @return array + */ + protected function get_other_values(renderer_base $output): array { + return [ + 'menucards' => $this->related['menucards'] + ]; + } +} diff --git a/reportbuilder/classes/external/reports/delete.php b/reportbuilder/classes/external/reports/delete.php new file mode 100644 index 00000000000..c00e06dc2a0 --- /dev/null +++ b/reportbuilder/classes/external/reports/delete.php @@ -0,0 +1,84 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\external\reports; + +use external_api; +use external_function_parameters; +use external_value; +use core_reportbuilder\permission; +use core_reportbuilder\local\helpers\report; +use core_reportbuilder\local\models\report as report_model; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once("{$CFG->libdir}/externallib.php"); + +/** + * External method for deleting reports + * + * @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'), + ]); + } + + /** + * External method execution + * + * @param int $reportid + * @return bool + */ + public static function execute(int $reportid): bool { + [ + 'reportid' => $reportid, + ] = self::validate_parameters(self::execute_parameters(), [ + 'reportid' => $reportid, + ]); + + // Load the report model for deletion. Note we don't use the manager class because it validates the report source, + // and we want user to be able to delete report, even if it's no longer associated with a valid source. + $report = new report_model($reportid); + + self::validate_context($report->get_context()); + permission::require_can_edit_report($report); + + return report::delete_report($reportid); + } + + /** + * External method return value + * + * @return external_value + */ + public static function execute_returns(): external_value { + return new external_value(PARAM_BOOL, 'Success'); + } +} diff --git a/reportbuilder/classes/local/helpers/report.php b/reportbuilder/classes/local/helpers/report.php index 89861c80df6..0a975549fb5 100644 --- a/reportbuilder/classes/local/helpers/report.php +++ b/reportbuilder/classes/local/helpers/report.php @@ -95,4 +95,141 @@ class report { return $report->delete(); } + + + /** + * Add given column to report + * + * @param int $reportid + * @param string $uniqueidentifier + * @return column + * @throws invalid_parameter_exception + */ + public static function add_report_column(int $reportid, string $uniqueidentifier): column { + $report = manager::get_report_from_id($reportid); + + if (!array_key_exists($uniqueidentifier, $report->get_columns())) { + throw new invalid_parameter_exception('Invalid column'); + } + + $column = new column(0, (object) [ + 'reportid' => $reportid, + 'uniqueidentifier' => $uniqueidentifier, + 'columnorder' => column::get_max_columnorder($reportid, 'columnorder') + 1, + 'sortorder' => column::get_max_columnorder($reportid, 'sortorder') + 1, + ]); + + return $column->create(); + } + + /** + * Delete given column from report + * + * @param int $reportid + * @param int $columnid + * @return bool + * @throws invalid_parameter_exception + */ + public static function delete_report_column(int $reportid, int $columnid): bool { + global $DB; + + $column = column::get_record(['id' => $columnid, 'reportid' => $reportid]); + if ($column === false) { + throw new invalid_parameter_exception('Invalid column'); + } + + // After deletion, re-index remaining report columns. + if ($result = $column->delete()) { + $sqlupdateorder = ' + UPDATE {' . column::TABLE . '} + SET columnorder = columnorder - 1 + WHERE reportid = :reportid + AND columnorder > :columnorder'; + + $DB->execute($sqlupdateorder, ['reportid' => $reportid, 'columnorder' => $column->get('columnorder')]); + } + + return $result; + } + + /** + * Re-order given column within a report + * + * @param int $reportid + * @param int $columnid + * @param int $position + * @return bool + * @throws invalid_parameter_exception + */ + public static function reorder_report_column(int $reportid, int $columnid, int $position): bool { + $column = column::get_record(['id' => $columnid, 'reportid' => $reportid]); + if ($column === false) { + throw new invalid_parameter_exception('Invalid column'); + } + + // Get the rest of the report columns, excluding the one we are moving. + $columns = column::get_records_select('reportid = :reportid AND id <> :id', [ + 'reportid' => $reportid, + 'id' => $columnid, + ], 'columnorder'); + + return static::reorder_persistents_by_field($column, $columns, $position, 'columnorder'); + } + + /** + * Get available columns for a given report + * + * @param report_model $persistent + * @return array + */ + public static function get_available_columns(report_model $persistent) : array { + $available = []; + + $report = manager::get_report_from_persistent($persistent); + + // Get current report columns. + foreach ($report->get_columns() as $column) { + $entityname = $column->get_entity_name(); + $entitytitle = $column->get_title(); + if (!array_key_exists($entityname, $available)) { + $available[$entityname] = [ + 'name' => (string) $report->get_entity_title($entityname), + 'key' => $entityname, + 'items' => [], + ]; + } + + $available[$entityname]['items'][] = [ + 'name' => $entitytitle, + 'identifier' => $column->get_unique_identifier(), + 'title' => get_string('addcolumn', 'core_reportbuilder', $entitytitle), + 'action' => 'report-add-column' + ]; + } + + return array_values($available); + } + + /** + * Helper method for re-ordering given persistents (columns, filters, etc) + * + * @param persistent $persistent The persistent we are moving + * @param persistent[] $persistents The rest of the persistents + * @param string $field The field we need to update + * @return bool + */ + private static function reorder_persistents_by_field(persistent $persistent, array $persistents, int $position, + string $field): bool { + + // Splice into new position. + array_splice($persistents, $position - 1, 0, [$persistent]); + + $fieldorder = 1; + foreach ($persistents as $persistent) { + $persistent->set($field, $fieldorder++) + ->update(); + } + + return true; + } } diff --git a/reportbuilder/classes/local/report/base.php b/reportbuilder/classes/local/report/base.php index 5d0eedfd41e..d43189de344 100644 --- a/reportbuilder/classes/local/report/base.php +++ b/reportbuilder/classes/local/report/base.php @@ -281,6 +281,20 @@ abstract class base { $this->entitytitles[$name] = $title; } + /** + * Returns title of given report entity + * + * @return lang_string + * @throws coding_exception + */ + final public function get_entity_title(string $name): lang_string { + if (!array_key_exists($name, $this->entitytitles)) { + throw new coding_exception('Invalid entity name', $name); + } + + return $this->entitytitles[$name]; + } + /** * Adds a column to the report * @@ -431,6 +445,15 @@ abstract class base { }); } + /** + * Return all active report filters (by default, all available filters) + * + * @return filter[] + */ + public function get_active_filters(): array { + return $this->get_filters(); + } + /** * Return all report filter instances * @@ -442,7 +465,7 @@ abstract class base { $filterclass = $filter->get_filter_class(); return $filterclass::create($filter); - }, $this->get_filters()); + }, $this->get_active_filters()); } /** diff --git a/reportbuilder/classes/local/report/column.php b/reportbuilder/classes/local/report/column.php index af1d5fb477e..a854f7976d8 100644 --- a/reportbuilder/classes/local/report/column.php +++ b/reportbuilder/classes/local/report/column.php @@ -21,6 +21,7 @@ namespace core_reportbuilder\local\report; use coding_exception; use lang_string; use core_reportbuilder\local\helpers\database; +use core_reportbuilder\local\models\column as column_model; /** * Class to represent a report column @@ -85,6 +86,9 @@ final class column { /** @var bool $available Used to know if column is available to the current user or not */ protected $available = true; + /** @var column_model $persistent */ + protected $persistent; + /** * Column constructor * @@ -541,4 +545,25 @@ final class column { $this->available = $available; return $this; } + + + /** + * Set column persistent + * + * @param column_model $persistent + * @return self + */ + public function set_persistent(column_model $persistent): self { + $this->persistent = $persistent; + return $this; + } + + /** + * Return column persistent + * + * @return mixed + */ + public function get_persistent(): column_model { + return $this->persistent; + } } diff --git a/reportbuilder/classes/local/systemreports/reports_list.php b/reportbuilder/classes/local/systemreports/reports_list.php index 1bc196b48fb..0ee545163c1 100644 --- a/reportbuilder/classes/local/systemreports/reports_list.php +++ b/reportbuilder/classes/local/systemreports/reports_list.php @@ -225,6 +225,17 @@ class reports_list extends system_report { * Add actions to report */ protected function add_actions(): void { + // Edit content action. + $this->add_action((new action( + new moodle_url('/reportbuilder/edit.php', ['id' => ':id']), + new pix_icon('t/right', get_string('editreportcontent', 'core_reportbuilder')) + )) + ->add_callback(function(stdClass $row): bool { + return $this->report_source_valid($row->source) && permission::can_edit_report($this->get_report_from_row($row)); + }) + ); + + // Edit details action. $this->add_action((new action( new moodle_url('#'), new pix_icon('t/edit', get_string('editreportdetails', 'core_reportbuilder')), @@ -235,6 +246,7 @@ class reports_list extends system_report { }) ); + // Delete action. $this->add_action((new action( new moodle_url('#'), new pix_icon('t/delete', get_string('deletereport', 'core_reportbuilder')), diff --git a/reportbuilder/classes/output/column_heading_editable.php b/reportbuilder/classes/output/column_heading_editable.php new file mode 100644 index 00000000000..1b6a6cf97a5 --- /dev/null +++ b/reportbuilder/classes/output/column_heading_editable.php @@ -0,0 +1,81 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\output; + +use core_external; +use core\output\inplace_editable; +use core_reportbuilder\manager; +use core_reportbuilder\permission; +use core_reportbuilder\local\models\column; + +/** + * Column heading editable component + * + * @package core_reportbuilder + * @copyright 2021 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class column_heading_editable extends inplace_editable { + + /** + * Class constructor + * + * @param int $columnid + * @param column|null $column + */ + public function __construct(int $columnid, ?column $column = null) { + if ($column === null) { + $column = new column($columnid); + } + + $report = $column->get_report(); + $editable = permission::can_edit_report($report); + + $columninstance = manager::get_report_from_persistent($report) + ->get_column($column->get('uniqueidentifier')); + + $displayvalue = $column->get('heading') ?: $columninstance->get_title(); + + parent::__construct('core_reportbuilder', 'columnheading', $column->get('id'), $editable, $displayvalue, $displayvalue, + get_string('renamecolumn', 'core_reportbuilder', $columninstance->get_title())); + } + + /** + * Update column persistent and return self, called from inplace_editable callback + * + * @param int $columnid + * @param string $value + * @return self + */ + public static function update(int $columnid, string $value): self { + $column = new column($columnid); + + $report = $column->get_report(); + + core_external::validate_context($report->get_context()); + permission::require_can_edit_report($report); + + $value = clean_param($value, PARAM_TEXT); + $column + ->set('heading', $value) + ->update(); + + return new self(0, $column); + } +} diff --git a/reportbuilder/classes/output/custom_report.php b/reportbuilder/classes/output/custom_report.php new file mode 100644 index 00000000000..1037ea07aeb --- /dev/null +++ b/reportbuilder/classes/output/custom_report.php @@ -0,0 +1,70 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\output; + +use core_reportbuilder\external\custom_report_exporter; +use core_reportbuilder\local\models\report; +use renderable; +use renderer_base; +use stdClass; +use templatable; + +/** + * Custom report output class + * + * @package core_reportbuilder + * @copyright 2021 David Matamoros + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class custom_report implements renderable, templatable { + + /** @var report $reportpersistent */ + protected $persistent; + + /** @var bool $editmode */ + protected $editmode; + + /** @var bool $showeditbutton */ + protected $showeditbutton; + + /** + * Class constructor + * + * @param report $reportpersistent + * @param bool $editmode + * @param bool $showeditbutton + */ + public function __construct(report $reportpersistent, bool $editmode = true, bool $showeditbutton = true) { + $this->persistent = $reportpersistent; + $this->editmode = $editmode; + $this->showeditbutton = $showeditbutton; + } + + /** + * Export report data suitable for a template + * + * @param renderer_base $output + * @return stdClass + */ + public function export_for_template(renderer_base $output): stdClass { + $exporter = new custom_report_exporter($this->persistent, [], $this->editmode, $this->showeditbutton); + + return $exporter->export($output); + } +} diff --git a/reportbuilder/classes/output/renderer.php b/reportbuilder/classes/output/renderer.php index c184d9ff54e..b4d9d7312b2 100644 --- a/reportbuilder/classes/output/renderer.php +++ b/reportbuilder/classes/output/renderer.php @@ -20,6 +20,7 @@ namespace core_reportbuilder\output; use html_writer; use plugin_renderer_base; +use core_reportbuilder\table\custom_report_table; use core_reportbuilder\table\system_report_table; /** @@ -58,6 +59,21 @@ class renderer extends plugin_renderer_base { return $output; } + /** + * Render a custom report table + * + * @param custom_report_table $table + * @return string + */ + protected function render_custom_report_table(custom_report_table $table): string { + ob_start(); + $table->out($table->get_default_per_page(), false); + $output = ob_get_contents(); + ob_end_clean(); + + return $output; + } + /** * Renders the New report button * diff --git a/reportbuilder/classes/table/base_report_table.php b/reportbuilder/classes/table/base_report_table.php new file mode 100644 index 00000000000..afe3215d956 --- /dev/null +++ b/reportbuilder/classes/table/base_report_table.php @@ -0,0 +1,161 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\table; + +use context; +use moodle_url; +use renderable; +use table_sql; +use core_table\dynamic; +use core_reportbuilder\local\helpers\database; +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; + +defined('MOODLE_INTERNAL') || die; + +require_once("{$CFG->libdir}/tablelib.php"); + +/** + * Base report dynamic table class + * + * @package core_reportbuilder + * @copyright 2021 David Matamoros + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class base_report_table extends table_sql implements dynamic, renderable { + + /** @var report $persistent */ + protected $persistent; + + /** @var base_report $report */ + protected $report; + + /** @var string $groupbysql */ + protected $groupbysql = ''; + + /** + * Initialises table SQL properties + * + * @param string $fields + * @param string $from + * @param array $joins + * @param string $where + * @param array $params + * @param array $groupby + */ + protected function init_sql(string $fields, string $from, array $joins, string $where, array $params, + array $groupby = []): void { + + $wheres = []; + if ($where !== '') { + $wheres[] = $where; + } + + // 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); + } + } + + $wheresql = '1=1'; + if (!empty($wheres)) { + $wheresql = implode(' AND ', $wheres); + } + + if (!empty($groupby)) { + $this->groupbysql = 'GROUP BY ' . implode(', ', $groupby); + } + + // Add unique table joins. + $from .= ' ' . implode(' ', array_unique($joins)); + + $this->set_sql($fields, $from, $wheresql, $params); + + $counttablealias = database::generate_alias(); + $this->set_count_sql(" + SELECT COUNT(1) + FROM (SELECT {$fields} + FROM {$from} + WHERE {$wheresql} + {$this->groupbysql} + ) {$counttablealias}", $params); + } + + /** + * Return SQL fragments from given filter instance suitable for inclusion in table SQL + * + * @param filter $filter + * @param array $filtervalues + * @return array [$sql, $params] + */ + private function get_filter_sql(filter $filter, array $filtervalues): array { + /** @var base $filterclass */ + $filterclass = $filter->get_filter_class(); + + return $filterclass::create($filter)->get_sql_filter($filtervalues); + } + + /** + * Override parent method of the same, to make use of a recordset and avoid issues with duplicate values in the first column + * + * @param int $pagesize + * @param bool $useinitialsbar + */ + public function query_db($pagesize, $useinitialsbar = true) { + global $DB; + + $sql = "SELECT {$this->sql->fields} FROM {$this->sql->from} WHERE {$this->sql->where} {$this->groupbysql}"; + + $sort = $this->get_sql_sort(); + if ($sort) { + $sql .= " ORDER BY {$sort}"; + } + + if (!$this->is_downloading()) { + $this->pagesize($pagesize, $DB->count_records_sql($this->countsql, $this->countparams)); + + $this->rawdata = $DB->get_recordset_sql($sql, $this->sql->params, $this->get_page_start(), $this->get_page_size()); + } else { + $this->rawdata = $DB->get_recordset_sql($sql, $this->sql->params); + } + } + + /** + * Get the context for the table (that of the report persistent) + * + * @return context + */ + public function get_context(): context { + return $this->persistent->get_context(); + } + + /** + * Set the base URL of the table to the current page URL + */ + public function guess_base_url(): void { + $this->baseurl = new moodle_url('/'); + } +} diff --git a/reportbuilder/classes/table/custom_report_table.php b/reportbuilder/classes/table/custom_report_table.php new file mode 100644 index 00000000000..00a1bb7ba0e --- /dev/null +++ b/reportbuilder/classes/table/custom_report_table.php @@ -0,0 +1,262 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\table; + +use html_writer; +use moodle_exception; +use moodle_url; +use stdClass; +use core_reportbuilder\manager; +use core_reportbuilder\local\models\column as column_model; +use core_reportbuilder\local\models\report; +use core_reportbuilder\local\report\column; +use core_reportbuilder\output\column_aggregation_editable; +use core_reportbuilder\output\column_heading_editable; + +/** + * Custom report dynamic table class + * + * @package core_reportbuilder + * @copyright 2021 David Matamoros + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class custom_report_table extends base_report_table { + + /** @var string Unique ID prefix for the table */ + private const UNIQUEID_PREFIX = 'custom-report-table-'; + + /** + * 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 + * + * @param string $uniqueid + * @throws moodle_exception For invalid unique ID + */ + public function __construct(string $uniqueid) { + if (!preg_match('/^' . self::UNIQUEID_PREFIX . '(?\d+)$/', $uniqueid, $matches)) { + throw new moodle_exception('invalidcustomreportid', 'core_reportbuilder', '', null, $uniqueid); + } + + parent::__construct($uniqueid); + + $this->define_baseurl(new moodle_url('/reportbuilder/edit.php', ['id' => $matches['id']])); + + // Load the report persistent, and accompanying system report instance. + $this->persistent = new report($matches['id']); + $this->report = manager::get_report_from_persistent($this->persistent); + + $fields = $groupby = []; + $maintable = $this->report->get_main_table(); + $maintablealias = $this->report->get_main_table_alias(); + $joins = $this->report->get_joins(); + [$where, $params] = $this->report->get_base_condition(); + + $this->set_attribute('data-region', 'reportbuilder-table'); + $this->set_attribute('class', $this->attributes['class'] . ' reportbuilder-table'); + + // Retrieve all report columns, exit early if there are none. + $columns = $this->get_active_columns(); + if (empty($columns)) { + $this->init_sql('*', "{{$maintable}} {$maintablealias}", [], '1=0', []); + return; + } + + $columnheaders = []; + foreach ($columns as $column) { + $columnheaders[$column->get_column_alias()] = $column->get_persistent()->get('heading') ?: $column->get_title(); + + // Add each columns fields, joins and params to our report. + $fields = array_merge($fields, $column->get_fields()); + $joins = array_merge($joins, $column->get_joins()); + $params = array_merge($params, $column->get_params()); + + // Disable sorting for some columns. + if (!$column->get_is_sortable()) { + $this->no_sorting($column->get_column_alias()); + } + } + + $this->define_columns(array_keys($columnheaders)); + $this->define_headers(array_values($columnheaders)); + + // Table configuration. + $this->initialbars(false); + $this->collapsible(false); + $this->pageable(true); + + // Initialise table SQL properties. + $fieldsql = implode(', ', $fields); + $this->init_sql($fieldsql, "{{$maintable}} {$maintablealias}", $joins, $where, $params, $groupby); + } + + /** + * Return a new instance of the class for given report ID + * + * @param int $reportid + * @return static + */ + public static function create(int $reportid): self { + return new static(self::UNIQUEID_PREFIX . $reportid); + } + + /** + * Get user preferred sort columns, overriding those of parent. If user has no preferences then use report defaults + * + * @return array + */ + public function get_sort_columns() { + $sortcolumns = parent::get_sort_columns(); + if (empty($sortcolumns)) { + $columns = $this->get_active_columns(); + + $instances = column_model::get_records([ + 'reportid' => $this->report->get_report_persistent()->get('id'), + 'sortenabled' => 1, + ], 'sortorder'); + + foreach ($instances as $instance) { + $column = $columns[$instance->get('id')] ?? null; + if ($column !== null && $column->get_is_available()) { + $sortcolumns[$column->get_column_alias()] = $instance->get('sortdirection'); + } + } + } + + return $sortcolumns; + } + + /** + * Format each row of returned data, executing defined callbacks for the row and each column + * + * @param array|stdClass $row + * @return array + */ + public function format_row($row) { + $columns = $this->get_active_columns(); + + $formattedrow = []; + foreach ($columns as $column) { + $formattedrow[$column->get_column_alias()] = $column->format_value((array) $row); + } + + return $formattedrow; + } + + /** + * Get the html for the download buttons + * + * @return string + */ + public function download_buttons(): string { + // TODO. + return ''; + } + + /** + * Get the columns of the custom report, returned instances being valid and available for the user + * + * @return column[] Indexed by column ID + */ + private function get_active_columns(): array { + $columns = []; + + $instances = column_model::get_records(['reportid' => $this->report->get_report_persistent()->get('id')], 'columnorder'); + foreach ($instances as $index => $instance) { + $column = $this->report->get_column($instance->get('uniqueidentifier')); + if ($column !== null && $column->get_is_available()) { + $column->set_persistent($instance); + // We should clone the report column to ensure if it's added twice to a report, each operates independently. + $columns[$instance->get('id')] = clone $column + ->set_index($index); + } + } + + return $columns; + } + + public function print_headers() { + global $OUTPUT, $PAGE; + + $columns = $this->get_active_columns(); + if (empty($columns)) { + $this->print_nothing_to_display(); + return; + } + + $columns = array_values($columns); + $renderer = $PAGE->get_renderer('core'); + + echo html_writer::start_tag('thead'); + echo html_writer::start_tag('tr'); + + foreach ($this->headers as $index => $title) { + $column = $columns[$index]; + + $headingeditable = new column_heading_editable(0, $column->get_persistent()); + + // Render table header cell, with all editing controls. + $headercell = $OUTPUT->render_from_template('core_reportbuilder/table_header_cell', [ + 'entityname' => $this->report->get_entity_title($column->get_entity_name()), + 'name' => $column->get_title(), + 'headingeditable' => $headingeditable->render($renderer), + 'aggregationeditable' => 'Aggregation', + 'movetitle' => get_string('movecolumn', 'core_reportbuilder', $column->get_title()), + ]); + + echo html_writer::tag('th', $headercell, [ + 'class' => 'border-right border-left', + 'scope' => 'col', + 'data-region' => 'column-header', + 'data-column-id' => $column->get_persistent()->get('id'), + 'data-column-name' => $column->get_title(), + 'data-column-position' => $index + 1, + ]); + } + + echo html_writer::end_tag('tr'); + echo html_writer::end_tag('thead'); + } + + /** + * Override start of HTML to remove top pagination + */ + public function start_html() { + // Render the dynamic table header. + echo $this->get_dynamic_table_html_start(); + + // Render button to allow user to reset table preferences. + echo $this->render_reset_button(); + + $this->wrap_html_start(); + + echo html_writer::start_tag('div', array('class' => 'no-overflow')); + echo html_writer::start_tag('table', $this->attributes); + } + + /** + * Override end of HTML to ensure that column headers are always added + */ + public function finish_html() { + if ($this->totalrows == 0 && !$this->started_output) { + $this->start_output(); + } + + parent::finish_html(); + } +} diff --git a/reportbuilder/classes/table/custom_report_table_filterset.php b/reportbuilder/classes/table/custom_report_table_filterset.php new file mode 100644 index 00000000000..5c7ad871def --- /dev/null +++ b/reportbuilder/classes/table/custom_report_table_filterset.php @@ -0,0 +1,40 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\table; + +use core_table\local\filter\filterset; + +/** + * Custom report dynamic table filterset class + * + * @package core_reportbuilder + * @copyright 2021 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class custom_report_table_filterset extends filterset { + + /** + * Get the required filters + * + * @return array. + */ + public function get_required_filters(): array { + return []; + } +} diff --git a/reportbuilder/classes/table/system_report_table.php b/reportbuilder/classes/table/system_report_table.php index f4471bf3425..28d09abc6b9 100644 --- a/reportbuilder/classes/table/system_report_table.php +++ b/reportbuilder/classes/table/system_report_table.php @@ -18,26 +18,17 @@ declare(strict_types=1); namespace core_reportbuilder\table; -use context; +use core_table\local\filter\filterset; use html_writer; use moodle_exception; -use moodle_url; -use renderable; -use table_sql; use stdClass; -use core_table\dynamic; use core_reportbuilder\manager; -use core_reportbuilder\system_report; -use core_reportbuilder\local\filters\base; use core_reportbuilder\local\models\report; use core_reportbuilder\local\report\action; use core_reportbuilder\local\report\column; -use core_table\local\filter\filterset; defined('MOODLE_INTERNAL') || die; -require_once("{$CFG->libdir}/tablelib.php"); - /** * System report dynamic table class * @@ -45,17 +36,11 @@ require_once("{$CFG->libdir}/tablelib.php"); * @copyright 2020 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class system_report_table extends table_sql implements dynamic, renderable { +class system_report_table extends base_report_table { /** @var string Unique ID prefix for the table */ private const UNIQUEID_PREFIX = 'system-report-table-'; - /** @var report $report */ - protected $report; - - /** @var system_report $systemreport */ - protected $systemreport; - /** * Table constructor. Note that the passed unique ID value must match the pattern "system-report-table-(\d+)" so that * dynamic updates continue to load the same report @@ -72,27 +57,27 @@ class system_report_table extends table_sql implements dynamic, renderable { parent::__construct($uniqueid); // Load the report persistent, and accompanying system report instance. - $this->report = new report($matches['id']); - $this->systemreport = manager::get_report_from_persistent($this->report, $parameters); + $this->persistent = new report($matches['id']); + $this->report = manager::get_report_from_persistent($this->persistent, $parameters); - $fields = $this->systemreport->get_base_fields(); - $maintable = $this->systemreport->get_main_table(); - $maintablealias = $this->systemreport->get_main_table_alias(); - $joins = $this->systemreport->get_joins(); - [$where, $params] = $this->systemreport->get_base_condition(); + $fields = $this->report->get_base_fields(); + $maintable = $this->report->get_main_table(); + $maintablealias = $this->report->get_main_table_alias(); + $joins = $this->report->get_joins(); + [$where, $params] = $this->report->get_base_condition(); $this->set_attribute('data-region', 'reportbuilder-table'); $this->set_attribute('class', $this->attributes['class'] . ' reportbuilder-table'); // Download options. $this->showdownloadbuttonsat = [TABLE_P_BOTTOM]; - $this->is_downloading($parameters['download'] ?? null, $this->systemreport->get_downloadfilename()); + $this->is_downloading($parameters['download'] ?? null, $this->report->get_downloadfilename()); // Retrieve all report columns. If we are downloading the report, remove as required. - $columns = $this->systemreport->get_columns(); + $columns = $this->report->get_columns(); if ($this->is_downloading()) { $columns = array_diff_key($columns, - array_flip($this->systemreport->get_exclude_columns_for_download())); + array_flip($this->report->get_exclude_columns_for_download())); } $columnheaders = []; @@ -119,7 +104,7 @@ class system_report_table extends table_sql implements dynamic, renderable { } // If the report has any actions then append appropriate column, note that actions are excluded during download. - if ($this->systemreport->has_actions() && !$this->is_downloading()) { + if ($this->report->has_actions() && !$this->is_downloading()) { $columnheaders['actions'] = html_writer::tag('span', get_string('actions', 'core_reportbuilder'), [ 'class' => 'sr-only', ]); @@ -130,15 +115,15 @@ class system_report_table extends table_sql implements dynamic, renderable { $this->define_headers(array_values($columnheaders)); // Initial table sort column. - if ($sortcolumn = $this->systemreport->get_initial_sort_column()) { - $this->sortable(true, $sortcolumn->get_column_alias(), $this->systemreport->get_initial_sort_direction()); + if ($sortcolumn = $this->report->get_initial_sort_column()) { + $this->sortable(true, $sortcolumn->get_column_alias(), $this->report->get_initial_sort_direction()); } // Table configuration. $this->initialbars(false); $this->collapsible(false); $this->pageable(true); - $this->set_default_per_page($this->systemreport->get_default_per_page()); + $this->set_default_per_page($this->report->get_default_per_page()); // Initialise table SQL properties. $fieldsql = implode(', ', $fields); @@ -164,78 +149,11 @@ class system_report_table extends table_sql implements dynamic, renderable { */ public function set_filterset(filterset $filterset): void { $parameters = $filterset->get_filter('parameters')->current(); - $this->systemreport->set_parameters((array) json_decode($parameters, true)); + $this->report->set_parameters((array) json_decode($parameters, true)); parent::set_filterset($filterset); } - /** - * Initialises table SQL properties - * - * @param string $fields - * @param string $from - * @param array $joins - * @param string $where - * @param array $params - */ - protected function init_sql(string $fields, string $from, array $joins, string $where, array $params): void { - $wheres = []; - if ($where !== '') { - $wheres[] = $where; - } - - $filtervalues = $this->systemreport->get_filter_values(); - foreach ($this->systemreport->get_filters() as $filter) { - /** @var base $filterclass */ - $filterclass = $filter->get_filter_class(); - $filterinstance = $filterclass::create($filter); - - [$filtersql, $filterparams] = $filterinstance->get_sql_filter($filtervalues); - if ($filtersql !== '') { - $wheres[] = "({$filtersql})"; - $params = array_merge($params, $filterparams); - - $joins = array_merge($joins, $filter->get_joins()); - } - } - - $wheresql = '1=1'; - if (!empty($wheres)) { - $wheresql = implode(' AND ', $wheres); - } - - // Add unique table joins. - $from .= ' ' . implode(' ', array_unique($joins)); - - $this->set_sql($fields, $from, $wheresql, $params); - $this->set_count_sql("SELECT COUNT(1) FROM {$from} WHERE {$wheresql}", $params); - } - - /** - * Override parent method of the same, to make use of a recordset and avoid issues with duplicate values in the first column - * - * @param int $pagesize - * @param bool $useinitialsbar - */ - public function query_db($pagesize, $useinitialsbar = true) { - global $DB; - - $sql = "SELECT {$this->sql->fields} FROM {$this->sql->from} WHERE {$this->sql->where}"; - - $sort = $this->get_sql_sort(); - if ($sort) { - $sql .= " ORDER BY {$sort}"; - } - - if (!$this->is_downloading()) { - $this->pagesize($pagesize, $DB->count_records_sql($this->countsql, $this->countparams)); - - $this->rawdata = $DB->get_recordset_sql($sql, $this->sql->params, $this->get_page_start(), $this->get_page_size()); - } else { - $this->rawdata = $DB->get_recordset_sql($sql, $this->sql->params); - } - } - /** * Override parent method for retrieving row class with that defined by the system report * @@ -243,7 +161,7 @@ class system_report_table extends table_sql implements dynamic, renderable { * @return string */ public function get_row_class($row) { - return $this->systemreport->get_row_class((object) $row); + return $this->report->get_row_class((object) $row); } /** @@ -253,13 +171,13 @@ class system_report_table extends table_sql implements dynamic, renderable { * @return array */ public function format_row($row) { - $this->systemreport->row_callback((object) $row); + $this->report->row_callback((object) $row); /** @var column[] $columnsbyalias */ $columnsbyalias = []; // Create a lookup for convenience, indexed by column alias. - $columns = $this->systemreport->get_columns(); + $columns = $this->report->get_columns(); foreach ($columns as $column) { $columnsbyalias[$column->get_column_alias()] = $column; } @@ -272,7 +190,7 @@ class system_report_table extends table_sql implements dynamic, renderable { } }); - if ($this->systemreport->has_actions()) { + if ($this->report->has_actions()) { $row['actions'] = $this->format_row_actions((object) $row); } @@ -288,27 +206,11 @@ class system_report_table extends table_sql implements dynamic, renderable { private function format_row_actions(stdClass $row): string { $actions = array_map(static function(action $action) use ($row): string { return (string) $action->get_action_link($row); - }, $this->systemreport->get_actions()); + }, $this->report->get_actions()); return implode('', $actions); } - /** - * Get the context for the table (that of the report persistent) - * - * @return context - */ - public function get_context(): context { - return $this->report->get_context(); - } - - /** - * Set the base URL of the table to the current page URL - */ - public function guess_base_url(): void { - $this->baseurl = new moodle_url('/'); - } - /** * Get the html for the download buttons * @@ -317,14 +219,14 @@ class system_report_table extends table_sql implements dynamic, renderable { public function download_buttons(): string { global $OUTPUT; - if ($this->systemreport->can_be_downloaded() && !$this->is_downloading()) { + if ($this->report->can_be_downloaded() && !$this->is_downloading()) { return $OUTPUT->download_dataformat_selector( get_string('downloadas', 'table'), new \moodle_url('/reportbuilder/download.php'), 'download', [ - 'id' => $this->report->get('id'), - 'parameters' => json_encode($this->systemreport->get_parameters()), + 'id' => $this->persistent->get('id'), + 'parameters' => json_encode($this->report->get_parameters()), ] ); } diff --git a/reportbuilder/edit.php b/reportbuilder/edit.php new file mode 100644 index 00000000000..1f634d2342d --- /dev/null +++ b/reportbuilder/edit.php @@ -0,0 +1,56 @@ +. + +/** + * Edit a custom report + * + * @package core_reportbuilder + * @copyright 2021 David Matamoros + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +declare(strict_types=1); + +use core_reportbuilder\manager; +use core_reportbuilder\output\custom_report; +use core_reportbuilder\permission; + +require_once(__DIR__ . '/../config.php'); +require_once("{$CFG->libdir}/adminlib.php"); + +$reportid = required_param('id', PARAM_INT); + +admin_externalpage_setup('customreports', null, ['id' => $reportid], new moodle_url('/reportbuilder/edit.php')); + +$report = manager::get_report_from_id($reportid); +permission::require_can_edit_report($report->get_report_persistent()); + +$PAGE->set_context($report->get_context()); + +/** @var \core_reportbuilder\output\renderer $renderer */ +$renderer = $PAGE->get_renderer('core_reportbuilder'); + +$reportname = $report->get_report_persistent()->get_formatted_name(); +$PAGE->navbar->add($reportname); +$PAGE->set_title($reportname); +$PAGE->set_heading($reportname); + +echo $OUTPUT->header(); + +$export = (new custom_report($report->get_report_persistent()))->export_for_template($renderer); +echo $renderer->render_from_template('core_reportbuilder/custom_report', $export); + +echo $OUTPUT->footer(); diff --git a/reportbuilder/lib.php b/reportbuilder/lib.php index 06b50f453e5..b868c5b48c1 100644 --- a/reportbuilder/lib.php +++ b/reportbuilder/lib.php @@ -58,6 +58,9 @@ function core_reportbuilder_inplace_editable($itemtype, $itemid, $newvalue) { case 'reportname': return \core_reportbuilder\output\report_name_editable::update($itemid, $newvalue); + case 'columnheading': + return \core_reportbuilder\output\column_heading_editable::update($itemid, $newvalue); + default: return false; } diff --git a/reportbuilder/templates/custom_report.mustache b/reportbuilder/templates/custom_report.mustache new file mode 100644 index 00000000000..5e50edf2b26 --- /dev/null +++ b/reportbuilder/templates/custom_report.mustache @@ -0,0 +1,95 @@ +{{! + 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/custom_report + + Template for a custom report + + Example context (json): + { + "id": 1, + "source": "some\\class\\name", + "table": "table", + "sidebarmenucards": [{ + "name": "General", + "items": [{ + "name": "Manually added users", + "identifier": "core_reportbuilder:users" + }] + }] + } +}} +
+
+ {{#editmode}} + + {{> core_reportbuilder/local/sidebar-menu/area}} + {{/editmode}} + +
+
+ +
+ + {{{table}}} +
+
+
+ + +
+
+ +{{#js}} + require(['core_reportbuilder/editor', 'core_reportbuilder/report'], + function(editor, report) { + editor.init(); + report.init(); + }); +{{/js}} diff --git a/reportbuilder/templates/local/sidebar-menu/area.mustache b/reportbuilder/templates/local/sidebar-menu/area.mustache new file mode 100644 index 00000000000..ae2a083a16e --- /dev/null +++ b/reportbuilder/templates/local/sidebar-menu/area.mustache @@ -0,0 +1,59 @@ +{{! + 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/sidebar-menu/area + + Template for showing report sidebar menu. + + Example context (json): + { + "sidebarmenucards": [{ + "name": "General", + "items": [{ + "name": "Manually added users", + "identifier": "core_reportbuilder:users" + }] + }] + } +}} +
+
+
+ + {{#pix}} a/search, core {{/pix}} + +
+ +
+
+ {{#sidebarmenucards}} + {{#menucards}} + {{> core_reportbuilder/local/sidebar-menu/card}} + {{/menucards}} + {{/sidebarmenucards}} +
+
+ +{{#js}} + require(['core_reportbuilder/sidebar'], function(sidebar) { + sidebar.init(); + }); +{{/js}} diff --git a/reportbuilder/templates/local/sidebar-menu/card.mustache b/reportbuilder/templates/local/sidebar-menu/card.mustache new file mode 100644 index 00000000000..76764cf3e8a --- /dev/null +++ b/reportbuilder/templates/local/sidebar-menu/card.mustache @@ -0,0 +1,44 @@ +{{! + 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/sidebar-menu/card + + Template for showing a single report sidebar menu card. + + Example context (json): + { + "name": "General", + "items": [{ + "name": "Manually added users", + "identifier": "core_reportbuilder:users" + }] + } +}} + +
+ {{< core_reportbuilder/toggle_card }} + {{$id}}card_{{key}}{{/id}} + {{$header}}{{name}}{{/header}} + {{$body}} +
+ {{#items}} + {{> core_reportbuilder/local/sidebar-menu/card_item}} + {{/items}} +
+ {{/body}} + {{/ core_reportbuilder/toggle_card }} +
\ No newline at end of file diff --git a/reportbuilder/templates/local/sidebar-menu/card_item.mustache b/reportbuilder/templates/local/sidebar-menu/card_item.mustache new file mode 100644 index 00000000000..8c0d8198381 --- /dev/null +++ b/reportbuilder/templates/local/sidebar-menu/card_item.mustache @@ -0,0 +1,39 @@ +{{! + 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/sidebar-menu/card_item + + Template for showing a single element in a sidebar menu card. + + Example context (json): + { + "name": "Manually added users", + "identifier": "tool_reportbuilder:users" + } +}} + + {{{name}}} + + {{#pix}} t/add, core {{/pix}} + + \ No newline at end of file diff --git a/reportbuilder/templates/system_report.mustache b/reportbuilder/templates/system_report.mustache index 5cc996fe2a1..c6b1632bd56 100644 --- a/reportbuilder/templates/system_report.mustache +++ b/reportbuilder/templates/system_report.mustache @@ -44,6 +44,6 @@ {{#js}} require(['core_reportbuilder/report'], function(report) { - report.init('{{id}}'); + report.init(); }); {{/js}} diff --git a/reportbuilder/templates/table_header_cell.mustache b/reportbuilder/templates/table_header_cell.mustache new file mode 100644 index 00000000000..55148336499 --- /dev/null +++ b/reportbuilder/templates/table_header_cell.mustache @@ -0,0 +1,46 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_reportbuilder/table_header_cell + + This template renders the header of the columns + + Example context (json): + { + "entityname": "Stuff", + "name": "Demo", + "headingeditable": "Demo", + "movetitle": "Move column 'Demo'" + } +}} +
+
+ {{>core/drag_handle}} +
+
+ +
{{{headingeditable}}}
+ {{{aggregationeditable}}} +
+
diff --git a/reportbuilder/templates/toggle_card.mustache b/reportbuilder/templates/toggle_card.mustache new file mode 100644 index 00000000000..58767029b29 --- /dev/null +++ b/reportbuilder/templates/toggle_card.mustache @@ -0,0 +1,54 @@ +{{! + 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/toggle_card + + Template for empty_message + + Example context (json): + { + "collapsed": "collapsed", + "id": "htmlid", + "header": "Toggle card heading", + "buttontitle": "Expand toggle card", + "body": "This is the toggle card content" + } +}} +
+
+
+
+ {{$header}}{{/header}} {{$helpicon}}{{/helpicon}} +
+ +
+
+
+
+ {{$body}}{{/body}} +
+
+
\ No newline at end of file diff --git a/reportbuilder/tests/behat/behat_reportbuilder.php b/reportbuilder/tests/behat/behat_reportbuilder.php index 0ea4a6d2b53..f869a044b0f 100644 --- a/reportbuilder/tests/behat/behat_reportbuilder.php +++ b/reportbuilder/tests/behat/behat_reportbuilder.php @@ -19,8 +19,10 @@ declare(strict_types=1); // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php. require_once(__DIR__ . '/../../../lib/behat/behat_base.php'); +use core_reportbuilder\local\models\report; + /** - * Behat step definitions for Reportbuilder + * Behat step definitions for Report builder * * @package core_reportbuilder * @copyright 2021 Paul Holden @@ -28,6 +30,32 @@ require_once(__DIR__ . '/../../../lib/behat/behat_base.php'); */ class behat_reportbuilder extends behat_base { + /** + * Convert page names to URLs for steps like 'When I am on the "[identifier]" "[page type]" page'. + * + * Recognised page names are: + * | type | identifier | description | + * | Editor | Report name | Custom report editor | + * + * @param string $type + * @param string $identifier + * @return moodle_url + * @throws Exception for unrecognised report or page type + */ + protected function resolve_page_instance_url(string $type, string $identifier): moodle_url { + if (!$report = report::get_record(['name' => $identifier])) { + throw new Exception("Unknown report '{$identifier}'"); + } + + switch ($type) { + case 'Editor': + return new moodle_url('/reportbuilder/edit.php', ['id' => $report->get('id')]); + + default: + throw new Exception("Unrecognised reportbuilder page type '{$type}'"); + } + } + /** * Return the list of partial named selectors * @@ -38,6 +66,9 @@ class behat_reportbuilder extends behat_base { new behat_component_named_selector('Filter', [ ".//*[@data-region='filters-form']//*[@data-filter-for=%locator%]", ]), + new behat_component_named_selector('Condition', [ + ".//*[@data-region='conditions-form']//*[@data-condition-name=%locator%]", + ]), ]; } } diff --git a/reportbuilder/tests/behat/columneditor.feature b/reportbuilder/tests/behat/columneditor.feature new file mode 100644 index 00000000000..ac2af904751 --- /dev/null +++ b/reportbuilder/tests/behat/columneditor.feature @@ -0,0 +1,54 @@ +@core_reportbuilder @javascript +Feature: Manage custom report columns + In order to manage the columns of custom reports + As an admin + I need to add, edit and delete columns in a report + + Scenario: Add column 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 "Add column 'Full name'" "link" + Then I should see "Added column 'Full name'" + And I should see "Full name" in the "reportbuilder-table" "table" + + Scenario: Rename column 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 > Columns" exist: + | report | uniqueidentifier | + | My report | user:fullname | + And I am on the "My report" "reportbuilder > Editor" page logged in as "admin" + When I set the field "Rename column 'Full name'" to "My renamed column" + And I reload the page + Then I should see "My renamed column" in the "reportbuilder-table" "table" + + Scenario: Move column 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 > Columns" exist: + | report | uniqueidentifier | + | My report | user:fullname | + | My report | user:email | + | My report | user:lastaccess | + And I am on the "My report" "reportbuilder > Editor" page logged in as "admin" + When I click on "Move column 'Last access'" "button" + And I click on "After \"Full name\"" "link" in the "Move column 'Last access'" "dialogue" + Then I should see "Moved column 'Last access'" + And "Last access" "text" should appear before "Email address" "text" + + Scenario: Delete column 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 > Columns" exist: + | report | uniqueidentifier | + | My report | user:fullname | + And I am on the "My report" "reportbuilder > Editor" page logged in as "admin" + When I click on "Delete column 'Full name'" "link" + And I click on "Delete" "button" in the "Delete column 'Full name'" "dialogue" + Then I should see "Deleted column 'Full name'" + And I should see "Nothing to display" diff --git a/reportbuilder/tests/external/columns/add_test.php b/reportbuilder/tests/external/columns/add_test.php new file mode 100644 index 00000000000..66a161070ff --- /dev/null +++ b/reportbuilder/tests/external/columns/add_test.php @@ -0,0 +1,98 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\external\columns; + +use core_reportbuilder_generator; +use external_api; +use externallib_advanced_testcase; +use core_reportbuilder\report_access_exception; +use core_reportbuilder\local\models\column; +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 columns + * + * @package core_reportbuilder + * @covers \core_reportbuilder\external\columns\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 column. + $result = add::execute($report->get('id'), 'user:fullname'); + $result = external_api::clean_returnvalue(add::execute_returns(), $result); + + $this->assertTrue($result['hassortablecolumns']); + $this->assertCount(1, $result['sortablecolumns']); + $sortablecolumn = reset($result['sortablecolumns']); + $this->assertEquals('Full name', $sortablecolumn['title']); + $this->assertEquals(SORT_ASC, $sortablecolumn['sortdirection']); + $this->assertEquals(0, $sortablecolumn['sortenabled']); + $this->assertEquals(1, $sortablecolumn['sortorder']); + $this->assertEquals('t/uplong', $sortablecolumn['sorticon']['key']); + $this->assertEquals('moodle', $sortablecolumn['sorticon']['component']); + $str = get_string('columnsortdirectiondesc', 'core_reportbuilder', 'Full name'); + $this->assertEquals($str, $sortablecolumn['sorticon']['title']); + + // Assert report columns. + $columns = column::get_records(['reportid' => $report->get('id')]); + $this->assertCount(1, $columns); + $this->assertEquals('user:fullname', reset($columns)->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/columns/delete_test.php b/reportbuilder/tests/external/columns/delete_test.php new file mode 100644 index 00000000000..73810cab950 --- /dev/null +++ b/reportbuilder/tests/external/columns/delete_test.php @@ -0,0 +1,104 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\external\columns; + +use core_reportbuilder_generator; +use external_api; +use externallib_advanced_testcase; +use core_reportbuilder\report_access_exception; +use core_reportbuilder\local\models\column; +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 columns + * + * @package core_reportbuilder + * @covers \core_reportbuilder\external\columns\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 columns. + $columnfullname = $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:fullname']); + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:email']); + + // Delete the first column. + $result = delete::execute($report->get('id'), $columnfullname->get('id')); + $result = external_api::clean_returnvalue(delete::execute_returns(), $result); + + $this->assertTrue($result['hassortablecolumns']); + $this->assertCount(1, $result['sortablecolumns']); + $sortablecolumn = reset($result['sortablecolumns']); + $this->assertEquals('Email address', $sortablecolumn['title']); + $this->assertEquals(SORT_ASC, $sortablecolumn['sortdirection']); + $this->assertEquals(0, $sortablecolumn['sortenabled']); + $this->assertEquals(2, $sortablecolumn['sortorder']); + $this->assertEquals('t/uplong', $sortablecolumn['sorticon']['key']); + $this->assertEquals('moodle', $sortablecolumn['sorticon']['component']); + $str = get_string('columnsortdirectiondesc', 'core_reportbuilder', 'Email address'); + $this->assertEquals($str, $sortablecolumn['sorticon']['title']); + + // Assert report columns. + $columns = column::get_records(['reportid' => $report->get('id')]); + $this->assertCount(1, $columns); + $this->assertEquals('user:email', reset($columns)->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]); + $column = $generator->create_column(['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'), $column->get('id')); + } +} diff --git a/reportbuilder/tests/external/columns/reorder_test.php b/reportbuilder/tests/external/columns/reorder_test.php new file mode 100644 index 00000000000..f7cda55f86b --- /dev/null +++ b/reportbuilder/tests/external/columns/reorder_test.php @@ -0,0 +1,104 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\external\columns; + +use core_reportbuilder_generator; +use external_api; +use externallib_advanced_testcase; +use core_reportbuilder\report_access_exception; +use core_reportbuilder\local\models\column; +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 columns + * + * @package core_reportbuilder + * @covers \core_reportbuilder\external\columns\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 columns. + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:fullname']); + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:email']); + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:country']); + $columncity = $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:city']); + + // Move the city column to second position. + $result = reorder::execute($report->get('id'), $columncity->get('id'), 2); + $result = external_api::clean_returnvalue(reorder::execute_returns(), $result); + $this->assertTrue($result); + + // Assert report columns order. + $columns = column::get_records(['reportid' => $report->get('id')], 'columnorder'); + + $columnidentifiers = array_map(static function(column $column): string { + return $column->get('uniqueidentifier'); + }, $columns); + + $this->assertEquals([ + 'user:fullname', + 'user:city', + 'user:email', + 'user:country', + ], $columnidentifiers); + } + + /** + * 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]); + $column = $generator->create_column(['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'), $column->get('id'), 1); + } +} diff --git a/reportbuilder/tests/external/reports/delete_test.php b/reportbuilder/tests/external/reports/delete_test.php new file mode 100644 index 00000000000..7d65a0e29ab --- /dev/null +++ b/reportbuilder/tests/external/reports/delete_test.php @@ -0,0 +1,87 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\external\reports; + +use core_reportbuilder_generator; +use external_api; +use externallib_advanced_testcase; +use core_reportbuilder\report_access_exception; +use core_reportbuilder\local\models\report; +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 reports + * + * @package core_reportbuilder + * @covers \core_reportbuilder\external\reports\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, + ]); + + // Sanity test. + $this->assertCount(1, report::get_records()); + + $result = delete::execute($report->get('id')); + $result = external_api::clean_returnvalue(delete::execute_returns(), $result); + $this->assertTrue($result); + + // Retrieve reports. + $this->assertEmpty(report::get_records()); + } + + /** + * 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'); + delete::execute($report->get('id')); + } +} diff --git a/reportbuilder/tests/generator/behat_core_reportbuilder_generator.php b/reportbuilder/tests/generator/behat_core_reportbuilder_generator.php new file mode 100644 index 00000000000..3ee06cc5c66 --- /dev/null +++ b/reportbuilder/tests/generator/behat_core_reportbuilder_generator.php @@ -0,0 +1,88 @@ +. + +declare(strict_types=1); + +use core_reportbuilder\local\models\report; + +/** + * Behat data generator for Report builder + * + * @package core_reportbuilder + * @copyright 2021 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class behat_core_reportbuilder_generator extends behat_generator_base { + + /** + * Get a list of the entities that can be created for this component + * + * @return array[] + */ + protected function get_creatable_entities(): array { + return [ + 'Reports' => [ + 'datagenerator' => 'report', + 'required' => [ + 'name', + 'source', + ], + ], + 'Columns' => [ + 'datagenerator' => 'column', + 'required' => [ + 'report', + 'uniqueidentifier', + ], + 'switchids' => [ + 'report' => 'reportid', + ], + ], + 'Conditions' => [ + 'datagenerator' => 'condition', + 'required' => [ + 'report', + 'uniqueidentifier', + ], + 'switchids' => [ + 'report' => 'reportid', + ], + ], + 'Filters' => [ + 'datagenerator' => 'filter', + 'required' => [ + 'report', + 'uniqueidentifier', + ], + 'switchids' => [ + 'report' => 'reportid', + ], + ], + ]; + } + + /** + * Look up report ID from given name + * + * @param string $name + * @return int + */ + protected function get_report_id(string $name): int { + global $DB; + + return (int) $DB->get_field(report::TABLE, 'id', ['name' => $name], MUST_EXIST); + } +} diff --git a/reportbuilder/tests/generator/lib.php b/reportbuilder/tests/generator/lib.php new file mode 100644 index 00000000000..20de5d2f663 --- /dev/null +++ b/reportbuilder/tests/generator/lib.php @@ -0,0 +1,115 @@ +. + +declare(strict_types=1); + +use core_reportbuilder\local\helpers\report as helper; +use core_reportbuilder\local\models\column; +use core_reportbuilder\local\models\filter; +use core_reportbuilder\local\models\report; + +/** + * Report builder test generator + * + * @package core_reportbuilder + * @copyright 2021 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_reportbuilder_generator extends component_generator_base { + + /** + * Create report + * + * @param array|stdClass $record + * @return report + * @throws coding_exception + */ + public function create_report($record): report { + $record = (array) $record; + + if (!array_key_exists('name', $record)) { + throw new coding_exception('Record must contain \'name\' property'); + } + if (!array_key_exists('source', $record)) { + throw new coding_exception('Record must contain \'source\' property'); + } + + // Include default setup unless specifically disabled in passed record. + $default = (bool) ($record['default'] ?? true); + + return helper::create_report((object) $record, $default); + } + + /** + * Create report column + * + * @param array|stdClass $record + * @return column + * @throws coding_exception + */ + public function create_column($record): column { + $record = (array) $record; + + if (!array_key_exists('reportid', $record)) { + throw new coding_exception('Record must contain \'reportid\' property'); + } + if (!array_key_exists('uniqueidentifier', $record)) { + throw new coding_exception('Record must contain \'uniqueidentifier\' property'); + } + + return helper::add_report_column($record['reportid'], $record['uniqueidentifier']); + } + + /** + * Create report filter + * + * @param array|stdClass $record + * @return filter + * @throws coding_exception + */ + public function create_filter($record): filter { + $record = (array) $record; + + if (!array_key_exists('reportid', $record)) { + throw new coding_exception('Record must contain \'reportid\' property'); + } + if (!array_key_exists('uniqueidentifier', $record)) { + throw new coding_exception('Record must contain \'uniqueidentifier\' property'); + } + + return helper::add_report_filter($record['reportid'], $record['uniqueidentifier']); + } + + /** + * Create report condition + * + * @param array|stdClass $record + * @return filter + * @throws coding_exception + */ + public function create_condition($record): filter { + $record = (array) $record; + + if (!array_key_exists('reportid', $record)) { + throw new coding_exception('Record must contain \'reportid\' property'); + } + if (!array_key_exists('uniqueidentifier', $record)) { + throw new coding_exception('Record must contain \'uniqueidentifier\' property'); + } + + return helper::add_report_condition($record['reportid'], $record['uniqueidentifier']); + } +} diff --git a/user/classes/reportbuilder/datasource/users.php b/user/classes/reportbuilder/datasource/users.php index a8bd9604edf..3f2016e8c8f 100644 --- a/user/classes/reportbuilder/datasource/users.php +++ b/user/classes/reportbuilder/datasource/users.php @@ -58,5 +58,17 @@ class users extends datasource { // Add all columns from entities to be available in custom reports. $this->add_entity($userentity); + + $userentityname = $userentity->get_entity_name(); + $this->add_columns_from_entity($userentityname); + } + + /** + * Return the columns that will be added to the report once is created + * + * @return string[] + */ + public function get_default_columns(): array { + return ['user:fullname', 'user:username', 'user:email']; } }