diff --git a/admin/settings/reportbuilder.php b/admin/settings/reportbuilder.php new file mode 100644 index 00000000000..e9ccdc70d75 --- /dev/null +++ b/admin/settings/reportbuilder.php @@ -0,0 +1,44 @@ +. + +/** + * Report builder related settings. + * + * @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_admin\local\externalpage\accesscallback; +use core_reportbuilder\permission; + +defined('MOODLE_INTERNAL') || die; + +/** @var admin_root $ADMIN */ +$ADMIN->add('reports', new admin_category('reportbuilder', new lang_string('reportbuilder', 'core_reportbuilder'))); + +$ADMIN->add( + 'reportbuilder', new accesscallback( + 'customreports', + get_string('customreports', 'core_reportbuilder'), + (new moodle_url('/reportbuilder/index.php'))->out(), + static function(accesscallback $accesscallback): bool { + return permission::can_view_reports_list(); + } + ) +); diff --git a/lang/en/reportbuilder.php b/lang/en/reportbuilder.php index 54b9874263b..726238baae4 100644 --- a/lang/en/reportbuilder.php +++ b/lang/en/reportbuilder.php @@ -28,6 +28,11 @@ $string['coursefullnamewithlink'] = 'Course full name with link'; $string['courseidnumberewithlink'] = 'Course ID number with link'; $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['editreportdetails'] = 'Edit report details'; +$string['editreportname'] = 'Edit report name'; +$string['customreports'] = 'Custom reports'; $string['entitycourse'] = 'Course'; $string['entityuser'] = 'User'; $string['errorreportaccess'] = 'You can not view this report'; @@ -66,6 +71,9 @@ $string['filtersapplied'] = 'Filters applied'; $string['filtersappliedx'] = 'Filters ({$a})'; $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['newreport'] = 'New report'; $string['privacy:metadata:column'] = 'Report column definitions'; $string['privacy:metadata:column:uniqueidentifier'] = 'Unique identifier of the column'; $string['privacy:metadata:column:usercreated'] = 'The ID of the user who created the column'; @@ -79,8 +87,18 @@ $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['reportbuilder'] = 'Report builder'; +$string['reportcreated'] = 'Report created'; +$string['reportdeleted'] = 'Report deleted'; +$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['timeadded'] = 'Time added'; +$string['timecreated'] = 'Time created'; +$string['timemodified'] = 'Time modified'; $string['userfullnamewithlink'] = 'Full name with link'; $string['userfullnamewithpicture'] = 'Full name with picture'; $string['userfullnamewithpicturelink'] = 'Full name with picture and link'; diff --git a/lang/en/role.php b/lang/en/role.php index 3cd4c0b7a99..e02f2f0bed7 100644 --- a/lang/en/role.php +++ b/lang/en/role.php @@ -371,6 +371,9 @@ $string['rating:rate'] = 'Add ratings to items'; $string['rating:view'] = 'View the total rating you received'; $string['rating:viewany'] = 'View total ratings that anyone received'; $string['rating:viewall'] = 'View all raw ratings given by individuals'; +$string['reportbuilder:edit'] = 'Create/edit custom reports'; +$string['reportbuilder:editall'] = 'Create/edit all custom reports'; +$string['reportbuilder:view'] = 'View custom reports'; $string['resetrole'] = 'Reset'; $string['resettingrole'] = 'Resetting role \'{$a}\''; $string['restore:configure'] = 'Configure restore options'; diff --git a/lib/db/access.php b/lib/db/access.php index 4062f923901..cd6ec1d26dd 100644 --- a/lib/db/access.php +++ b/lib/db/access.php @@ -2627,4 +2627,31 @@ $capabilities = array( 'coursecreator' => CAP_ALLOW, ] ], + + // Allow users to view custom reports. + 'moodle/reportbuilder:view' => [ + 'captype' => 'read', + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => [ + 'user' => CAP_ALLOW, + ], + ], + + // Allow users to create/edit their own custom reports. + 'moodle/reportbuilder:edit' => [ + 'captype' => 'write', + 'riskbitmap' => RISK_PERSONAL, + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => [ + 'manager' => CAP_ALLOW, + ], + ], + + // Allow users to create/edit all custom reports. + 'moodle/reportbuilder:editall' => [ + 'captype' => 'write', + 'riskbitmap' => RISK_PERSONAL, + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => [], + ], ); diff --git a/lib/db/services.php b/lib/db/services.php index 712c46c8c39..7daf1f23503 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -2798,6 +2798,12 @@ $functions = array( 'type' => 'write', 'ajax' => true, ], + 'core_reportbuilder_reports_delete' => [ + 'classname' => 'core_reportbuilder\external\reports\delete', + 'description' => 'Delete report', + 'type' => 'write', + 'ajax' => true, + ], ); $services = array( diff --git a/reportbuilder/amd/build/local/repository/modals.min.js b/reportbuilder/amd/build/local/repository/modals.min.js new file mode 100644 index 00000000000..846e20380f9 --- /dev/null +++ b/reportbuilder/amd/build/local/repository/modals.min.js @@ -0,0 +1,2 @@ +define ("core_reportbuilder/local/repository/modals",["exports","core_form/modalform","core/str"],function(a,b,c){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.createReportModal=void 0;b=function(a){return a&&a.__esModule?a:{default:a}}(b);var d=function(a,d){var e=2.\n\n/**\n * Module to handle modal form requests\n *\n * @module core_reportbuilder/local/repository/modals\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\nimport ModalForm from 'core_form/modalform';\nimport {get_string as getString} from 'core/str';\n\n/**\n * Open the New Report modal form\n *\n * @param {EventTarget} triggerElement\n * @param {Promise} modalTitle\n * @param {Number} reportId\n * @return {ModalForm}\n */\nexport const createReportModal = (triggerElement, modalTitle, reportId = 0) => {\n return new ModalForm({\n modalConfig: {\n title: modalTitle,\n },\n formClass: 'core_reportbuilder\\\\form\\\\report',\n args: {\n id: reportId,\n },\n saveButtonText: getString('save', 'moodle'),\n returnFocus: triggerElement,\n });\n};\n"],"file":"modals.min.js"} \ No newline at end of file diff --git a/reportbuilder/amd/build/local/repository/reports.min.js b/reportbuilder/amd/build/local/repository/reports.min.js new file mode 100644 index 00000000000..9717ae896e7 --- /dev/null +++ b/reportbuilder/amd/build/local/repository/reports.min.js @@ -0,0 +1,2 @@ +define ("core_reportbuilder/local/repository/reports",["exports","core/ajax"],function(a,b){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.deleteReport=void 0;b=function(a){return a&&a.__esModule?a:{default:a}}(b);var c=function(a){return b.default.call([{methodname:"core_reportbuilder_reports_delete",args:{reportid:a}}])[0]};a.deleteReport=c}); +//# sourceMappingURL=reports.min.js.map diff --git a/reportbuilder/amd/build/local/repository/reports.min.js.map b/reportbuilder/amd/build/local/repository/reports.min.js.map new file mode 100644 index 00000000000..463645f80a2 --- /dev/null +++ b/reportbuilder/amd/build/local/repository/reports.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../../src/local/repository/reports.js"],"names":["deleteReport","reportId","Ajax","call","methodname","args","reportid"],"mappings":"gLAwBA,uDAQO,GAAMA,CAAAA,CAAY,CAAG,SAAAC,CAAQ,CAAI,CAMpC,MAAOC,WAAKC,IAAL,CAAU,CALD,CACZC,UAAU,CAAE,mCADA,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 report AJAX requests\n *\n * @module core_reportbuilder/local/repository/reports\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 * Delete given report\n *\n * @param {Number} reportId\n * @return {Promise}\n */\nexport const deleteReport = reportId => {\n const request = {\n methodname: 'core_reportbuilder_reports_delete',\n args: {reportid: reportId}\n };\n\n return Ajax.call([request])[0];\n};\n\n"],"file":"reports.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 3e84ec64249..ab4484959f5 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\"]"}};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:{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}); //# 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 908c8bab73f..28c2daf951f 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","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,CANK,CAAlB,CAkBAN,CAAS,CAACO,eAAV,CAA4B,SAAAC,CAAQ,kBAAOR,CAAS,CAACC,OAAV,CAAkBC,YAAzB,6BAAwDM,CAAxD,QAApC,C,UAEeR,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 }\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","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 diff --git a/reportbuilder/amd/build/reports_list.min.js b/reportbuilder/amd/build/reports_list.min.js new file mode 100644 index 00000000000..af03106cf2a --- /dev/null +++ b/reportbuilder/amd/build/reports_list.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/reports_list",["exports","core/event_dispatcher","core/notification","core/pending","core/str","core/toast","core_reportbuilder/local/events","core_reportbuilder/local/selectors","core_reportbuilder/local/repository/reports","core_reportbuilder/local/repository/modals"],function(a,b,c,d,e,f,g,h,i,j){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=void 0;c=m(c);d=m(d);g=l(g);h=l(h);function k(){if("function"!=typeof WeakMap)return null;var a=new WeakMap;k=function(){return a};return a}function l(a){if(a&&a.__esModule){return a}if(null===a||"object"!==_typeof(a)&&"function"!=typeof a){return{default:a}}var b=k();if(b&&b.has(a)){return b.get(a)}var c={},d=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var e in a){if(Object.prototype.hasOwnProperty.call(a,e)){var f=d?Object.getOwnPropertyDescriptor(a,e):null;if(f&&(f.get||f.set)){Object.defineProperty(c,e,f)}else{c[e]=a[e]}}}c.default=a;if(b){b.set(a,c)}return c}function m(a){return a&&a.__esModule?a:{default:a}}function n(a,b){return s(a)||r(a,b)||p(a,b)||o()}function o(){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 p(a,b){if(!a)return;if("string"==typeof a)return q(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 q(a,b)}function q(a,b){if(null==b||b>a.length)b=a.length;for(var c=0,d=Array(b);c.\n\n/**\n * Report builder reports list management\n *\n * @module core_reportbuilder/reports_list\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 {dispatchEvent} from 'core/event_dispatcher';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\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 {deleteReport} from 'core_reportbuilder/local/repository/reports';\nimport {createReportModal} from 'core_reportbuilder/local/repository/modals';\n\n/**\n * Initialise module\n */\nexport const init = () => {\n document.addEventListener('click', event => {\n const reportCreate = event.target.closest(reportSelectors.actions.reportCreate);\n if (reportCreate) {\n event.preventDefault();\n\n // Redirect user to editing interface for the report after submission.\n const reportModal = createReportModal(event.target, getString('newreport', 'core_reportbuilder'));\n reportModal.addEventListener(reportModal.events.FORM_SUBMITTED, event => {\n window.location.href = event.detail;\n });\n\n reportModal.show();\n }\n\n const reportEdit = event.target.closest(reportSelectors.actions.reportEdit);\n if (reportEdit) {\n event.preventDefault();\n\n // Reload current report page after submission.\n const reportModal = createReportModal(event.target, getString('editreportdetails', 'core_reportbuilder'),\n reportEdit.dataset.reportId);\n reportModal.addEventListener(reportModal.events.FORM_SUBMITTED, () => {\n const reportElement = event.target.closest(reportSelectors.regions.report);\n\n getString('reportupdated', 'core_reportbuilder')\n .then(addToast)\n .then(() => {\n dispatchEvent(reportEvents.tableReload, {preservePagination: true}, reportElement);\n return;\n })\n .catch(Notification.exception);\n });\n\n reportModal.show();\n }\n\n const reportDelete = event.target.closest(reportSelectors.actions.reportDelete);\n if (reportDelete) {\n event.preventDefault();\n\n getStrings([\n {key: 'deletereport', component: 'core_reportbuilder'},\n {key: 'deletereportconfirm', component: 'core_reportbuilder', param: reportDelete.dataset.reportName},\n {key: 'delete', component: 'moodle'},\n ]).then(([confirmTitle, confirmText, confirmButton]) => {\n Notification.confirm(confirmTitle, confirmText, confirmButton, null, () => {\n const pendingPromise = new Pending('core_reportbuilder/reports:delete');\n const reportElement = event.target.closest(reportSelectors.regions.report);\n\n deleteReport(reportDelete.dataset.reportId)\n .then(() => getString('reportdeleted', 'core_reportbuilder'))\n .then(addToast)\n .then(() => {\n pendingPromise.resolve();\n dispatchEvent(reportEvents.tableReload, {preservePagination: true}, reportElement);\n return;\n })\n .catch(Notification.exception);\n });\n return;\n }).catch(Notification.exception);\n }\n });\n};\n"],"file":"reports_list.min.js"} \ No newline at end of file diff --git a/reportbuilder/amd/src/local/repository/modals.js b/reportbuilder/amd/src/local/repository/modals.js new file mode 100644 index 00000000000..59144de52f2 --- /dev/null +++ b/reportbuilder/amd/src/local/repository/modals.js @@ -0,0 +1,48 @@ +// 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 modal form requests + * + * @module core_reportbuilder/local/repository/modals + * @package core_reportbuilder + * @copyright 2021 David Matamoros + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import ModalForm from 'core_form/modalform'; +import {get_string as getString} from 'core/str'; + +/** + * Open the New Report modal form + * + * @param {EventTarget} triggerElement + * @param {Promise} modalTitle + * @param {Number} reportId + * @return {ModalForm} + */ +export const createReportModal = (triggerElement, modalTitle, reportId = 0) => { + return new ModalForm({ + modalConfig: { + title: modalTitle, + }, + formClass: 'core_reportbuilder\\form\\report', + args: { + id: reportId, + }, + saveButtonText: getString('save', 'moodle'), + returnFocus: triggerElement, + }); +}; diff --git a/reportbuilder/amd/src/local/repository/reports.js b/reportbuilder/amd/src/local/repository/reports.js new file mode 100644 index 00000000000..f0d80e7089a --- /dev/null +++ b/reportbuilder/amd/src/local/repository/reports.js @@ -0,0 +1,41 @@ +// 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 report AJAX requests + * + * @module core_reportbuilder/local/repository/reports + * @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'; + +/** + * Delete given report + * + * @param {Number} reportId + * @return {Promise} + */ +export const deleteReport = reportId => { + const request = { + methodname: 'core_reportbuilder_reports_delete', + args: {reportid: reportId} + }; + + return Ajax.call([request])[0]; +}; + diff --git a/reportbuilder/amd/src/local/selectors.js b/reportbuilder/amd/src/local/selectors.js index 1c0972b3061..aa44777bbe4 100644 --- a/reportbuilder/amd/src/local/selectors.js +++ b/reportbuilder/amd/src/local/selectors.js @@ -37,7 +37,10 @@ const SELECTORS = { }, actions: { reportActionPopup: '[data-action="report-action-popup"]', - } + reportCreate: '[data-action="report-create"]', + reportEdit: '[data-action="report-edit"]', + reportDelete: '[data-action="report-delete"]', + }, }; /** diff --git a/reportbuilder/amd/src/reports_list.js b/reportbuilder/amd/src/reports_list.js new file mode 100644 index 00000000000..236c15957d1 --- /dev/null +++ b/reportbuilder/amd/src/reports_list.js @@ -0,0 +1,104 @@ +// 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 reports list management + * + * @module core_reportbuilder/reports_list + * @package core_reportbuilder + * @copyright 2021 David Matamoros + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +"use strict"; + +import {dispatchEvent} from 'core/event_dispatcher'; +import Notification from 'core/notification'; +import Pending from 'core/pending'; +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 {deleteReport} from 'core_reportbuilder/local/repository/reports'; +import {createReportModal} from 'core_reportbuilder/local/repository/modals'; + +/** + * Initialise module + */ +export const init = () => { + document.addEventListener('click', event => { + const reportCreate = event.target.closest(reportSelectors.actions.reportCreate); + if (reportCreate) { + event.preventDefault(); + + // Redirect user to editing interface for the report after submission. + const reportModal = createReportModal(event.target, getString('newreport', 'core_reportbuilder')); + reportModal.addEventListener(reportModal.events.FORM_SUBMITTED, event => { + window.location.href = event.detail; + }); + + reportModal.show(); + } + + const reportEdit = event.target.closest(reportSelectors.actions.reportEdit); + if (reportEdit) { + event.preventDefault(); + + // Reload current report page after submission. + const reportModal = createReportModal(event.target, getString('editreportdetails', 'core_reportbuilder'), + reportEdit.dataset.reportId); + reportModal.addEventListener(reportModal.events.FORM_SUBMITTED, () => { + const reportElement = event.target.closest(reportSelectors.regions.report); + + getString('reportupdated', 'core_reportbuilder') + .then(addToast) + .then(() => { + dispatchEvent(reportEvents.tableReload, {preservePagination: true}, reportElement); + return; + }) + .catch(Notification.exception); + }); + + reportModal.show(); + } + + const reportDelete = event.target.closest(reportSelectors.actions.reportDelete); + if (reportDelete) { + event.preventDefault(); + + getStrings([ + {key: 'deletereport', component: 'core_reportbuilder'}, + {key: 'deletereportconfirm', component: 'core_reportbuilder', param: reportDelete.dataset.reportName}, + {key: 'delete', component: 'moodle'}, + ]).then(([confirmTitle, confirmText, confirmButton]) => { + Notification.confirm(confirmTitle, confirmText, confirmButton, null, () => { + const pendingPromise = new Pending('core_reportbuilder/reports:delete'); + const reportElement = event.target.closest(reportSelectors.regions.report); + + deleteReport(reportDelete.dataset.reportId) + .then(() => getString('reportdeleted', 'core_reportbuilder')) + .then(addToast) + .then(() => { + pendingPromise.resolve(); + dispatchEvent(reportEvents.tableReload, {preservePagination: true}, reportElement); + return; + }) + .catch(Notification.exception); + }); + return; + }).catch(Notification.exception); + } + }); +}; diff --git a/reportbuilder/classes/datasource.php b/reportbuilder/classes/datasource.php new file mode 100644 index 00000000000..f257920536a --- /dev/null +++ b/reportbuilder/classes/datasource.php @@ -0,0 +1,43 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder; + +use coding_exception; +use core_reportbuilder\local\helpers\report; +use core_reportbuilder\local\models\filter as filter_model; +use core_reportbuilder\local\report\base; +use core_reportbuilder\local\report\column; +use core_reportbuilder\local\report\filter; + +/** + * Class datasource + * + * @package core_reportbuilder + * @copyright 2021 David Matamoros + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class datasource extends base { + + /** + * Return user friendly name of the datasource + * + * @return string + */ + abstract public static function get_name(): string; +} diff --git a/reportbuilder/classes/event/report_created.php b/reportbuilder/classes/event/report_created.php new file mode 100644 index 00000000000..56920ac58bb --- /dev/null +++ b/reportbuilder/classes/event/report_created.php @@ -0,0 +1,96 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\event; + +use core\event\base; +use core_reportbuilder\local\models\report; +use moodle_url; + +/** + * Report builder custom report created event class. + * + * @package core_reportbuilder + * @copyright 2021 David Matamoros + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + * @property-read array $other { + * Extra information about the event. + * + * - string name: The name of the report + * - string source: The report source class + * } + */ +class report_created extends base { + + /** + * Initialise the event data. + */ + protected function init() { + $this->data['objecttable'] = report::TABLE; + $this->data['crud'] = 'c'; + $this->data['edulevel'] = self::LEVEL_OTHER; + } + + /** + * Creates an instance from a report object + * + * @param report $report + * @return self + */ + public static function create_from_object(report $report): self { + $eventparams = [ + 'context' => $report->get_context(), + 'objectid' => $report->get('id'), + 'other' => [ + 'name' => $report->get('name'), + 'source' => $report->get('source'), + ] + ]; + $event = self::create($eventparams); + $event->add_record_snapshot($event->objecttable, $report->to_record()); + return $event; + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('reportcreated', 'core_reportbuilder'); + } + + /** + * Returns non-localised description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' created the custom report with id '$this->objectid'."; + } + + /** + * Returns relevant URL. + * + * @return moodle_url + */ + public function get_url(): moodle_url { + return new moodle_url('/reportbuilder/edit.php', ['id' => $this->objectid]); + } +} diff --git a/reportbuilder/classes/event/report_deleted.php b/reportbuilder/classes/event/report_deleted.php new file mode 100644 index 00000000000..785defcab79 --- /dev/null +++ b/reportbuilder/classes/event/report_deleted.php @@ -0,0 +1,86 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\event; + +use core\event\base; +use core_reportbuilder\local\models\report; + +/** + * Report builder custom report deleted event class. + * + * @package core_reportbuilder + * @copyright 2021 David Matamoros + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + * @property-read array $other { + * Extra information about the event. + * + * - string name: The name of the report + * - string source: The report source class + * } + */ +class report_deleted extends base { + + /** + * Initialise the event data. + */ + protected function init() { + $this->data['objecttable'] = report::TABLE; + $this->data['crud'] = 'd'; + $this->data['edulevel'] = self::LEVEL_OTHER; + } + + /** + * Creates an instance from a report object + * + * @param report $report + * @return self + */ + public static function create_from_object(report $report): self { + $eventparams = [ + 'context' => $report->get_context(), + 'objectid' => $report->get('id'), + 'other' => [ + 'name' => $report->get('name'), + 'source' => $report->get('source'), + ] + ]; + $event = self::create($eventparams); + $event->add_record_snapshot($event->objecttable, $report->to_record()); + return $event; + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('reportdeleted', 'core_reportbuilder'); + } + + /** + * Returns non-localised description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' deleted the custom report with id '$this->objectid'."; + } +} diff --git a/reportbuilder/classes/event/report_updated.php b/reportbuilder/classes/event/report_updated.php new file mode 100644 index 00000000000..d99b3b8d9d3 --- /dev/null +++ b/reportbuilder/classes/event/report_updated.php @@ -0,0 +1,96 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\event; + +use core\event\base; +use core_reportbuilder\local\models\report; +use moodle_url; + +/** + * Report builder custom report updated event class. + * + * @package core_reportbuilder + * @copyright 2021 David Matamoros + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + * @property-read array $other { + * Extra information about the event. + * + * - string name: The name of the report + * - string source: The report source class + * } + */ +class report_updated extends base { + + /** + * Initialise the event data. + */ + protected function init() { + $this->data['objecttable'] = report::TABLE; + $this->data['crud'] = 'u'; + $this->data['edulevel'] = self::LEVEL_OTHER; + } + + /** + * Creates an instance from a report object + * + * @param report $report + * @return self + */ + public static function create_from_object(report $report): self { + $eventparams = [ + 'context' => $report->get_context(), + 'objectid' => $report->get('id'), + 'other' => [ + 'name' => $report->get('name'), + 'source' => $report->get('source'), + ] + ]; + $event = self::create($eventparams); + $event->add_record_snapshot($event->objecttable, $report->to_record()); + return $event; + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('reportupdated', 'core_reportbuilder'); + } + + /** + * Returns non-localised description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' updated the custom report with id '$this->objectid'."; + } + + /** + * Returns relevant URL. + * + * @return moodle_url + */ + public function get_url(): moodle_url { + return new moodle_url('/reportbuilder/edit.php', ['id' => $this->objectid]); + } +} diff --git a/reportbuilder/classes/form/report.php b/reportbuilder/classes/form/report.php new file mode 100644 index 00000000000..303b84a222a --- /dev/null +++ b/reportbuilder/classes/form/report.php @@ -0,0 +1,150 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\form; + +use context; +use context_system; +use core_reportbuilder\permission; +use moodle_url; +use core_form\dynamic_form; +use core_reportbuilder\datasource; +use core_reportbuilder\manager; +use core_reportbuilder\local\helpers\report as reporthelper; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once("$CFG->libdir/formslib.php"); + +/** + * Report details form + * + * @package core_reportbuilder + * @copyright 2021 David Matamoros + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class report extends dynamic_form { + + /** + * Return instance of the custom report we are editing, or null when creating a new report + * + * @return datasource|null + */ + protected function get_custom_report(): ?datasource { + if ($reportid = $this->optional_param('id', 0, PARAM_INT)) { + /** @var datasource $customreport */ + $customreport = manager::get_report_from_id($reportid); + + return $customreport; + } + + return null; + } + + /** + * Return the context for the form, it should be that of the custom report itself, or system when creating a new report + * + * @return context + */ + public function get_context_for_dynamic_submission(): context { + if ($report = $this->get_custom_report()) { + return $report->get_context(); + } else { + return context_system::instance(); + } + } + + /** + * Ensure current user is able to use this form + * + * A {@see \core_reportbuilder\report_access_exception} will be thrown if they can't + */ + protected function check_access_for_dynamic_submission(): void { + $report = $this->get_custom_report(); + + if ($report) { + permission::require_can_edit_report($report->get_report_persistent()); + } else { + permission::require_can_create_report(); + } + } + + /** + * Form definition + */ + public function definition() { + $mform = $this->_form; + + $mform->addElement('hidden', 'id'); + $mform->setType('id', PARAM_INT); + + $mform->addElement('text', 'name', get_string('name')); + $mform->setType('name', PARAM_TEXT); + $mform->addRule('name', null, 'required', null, 'client'); + $mform->addRule('name', get_string('maximumchars', '', 255), 'maxlength', 255); + + // Allow user to select report source if creating a new report. + if (!$this->get_custom_report()) { + $default = ['' => ['' => get_string('selectareportsource', 'core_reportbuilder')]]; + $mform->addElement('selectgroups', 'source', get_string('reportsource', 'core_reportbuilder'), + array_merge($default, manager::get_report_datasources())); + $mform->addRule('source', null, 'required', null, 'client'); + $mform->addHelpButton('source', 'reportsource', 'core_reportbuilder'); + + $mform->addElement('advcheckbox', 'includedefaultsetup', get_string('includedefaultsetup', 'core_reportbuilder')); + $mform->setDefault('includedefaultsetup', 1); + $mform->addHelpButton('includedefaultsetup', 'includedefaultsetup', 'core_reportbuilder'); + } + } + + /** + * Process the form submission + * + * @return string The URL to advance to upon completion + */ + public function process_dynamic_submission() { + $data = $this->get_data(); + + if ($data->id) { + $reportpersistent = reporthelper::update_report($data); + } else { + $reportpersistent = reporthelper::create_report($data, (bool)$data->includedefaultsetup); + } + + return (new moodle_url('/reportbuilder/edit.php', ['id' => $reportpersistent->get('id')]))->out(false); + } + + /** + * Load in existing data as form defaults + */ + public function set_data_for_dynamic_submission(): void { + if ($report = $this->get_custom_report()) { + $this->set_data($report->get_report_persistent()->to_record()); + } + } + + /** + * URL of the page using this form + * + * @return moodle_url + */ + public function get_page_url_for_dynamic_submission(): moodle_url { + return new moodle_url('/reportbuilder/index.php'); + } +} diff --git a/reportbuilder/classes/local/helpers/report.php b/reportbuilder/classes/local/helpers/report.php new file mode 100644 index 00000000000..89861c80df6 --- /dev/null +++ b/reportbuilder/classes/local/helpers/report.php @@ -0,0 +1,98 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\local\helpers; + +use stdClass; +use invalid_parameter_exception; +use core\persistent; +use core_reportbuilder\datasource; +use core_reportbuilder\manager; +use core_reportbuilder\local\models\column; +use core_reportbuilder\local\models\filter; +use core_reportbuilder\local\models\report as report_model; + +/** + * Helper class for manipulating custom reports and their elements (columns, filters, conditions, etc) + * + * @package core_reportbuilder + * @copyright 2021 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class report { + + /** + * Create custom report + * + * @param stdClass $data + * @param bool $default If $default is set to true it will populate report with default layout as defined by the selected + * source. These include pre-defined columns, filters and conditions. + * @return report_model + */ + public static function create_report(stdClass $data, bool $default = true): report_model { + // TODO move this properties_definition validation into the persistents, or resolve MDL-71086. + $data = (object) array_merge(array_intersect_key((array) $data, report_model::properties_definition()), [ + 'type' => datasource::TYPE_CUSTOM_REPORT, + ]); + + $reportpersistent = manager::create_report_persistent($data); + + // Add datasource default columns, filters and conditions to the report. + if ($default) { + $source = $reportpersistent->get('source'); + /** @var datasource $datasource */ + $datasource = new $source($reportpersistent, []); + } + + return $reportpersistent; + } + + /** + * Update custom report + * + * @param stdClass $data + * @return report_model + */ + public static function update_report(stdClass $data): report_model { + $report = report_model::get_record(['id' => $data->id, 'type' => datasource::TYPE_CUSTOM_REPORT]); + if ($report === false) { + throw new invalid_parameter_exception('Invalid report'); + } + + $report->set('name', $data->name) + ->update(); + + return $report; + } + + /** + * Delete custom report + * + * @param int $reportid + * @return bool + * @throws invalid_parameter_exception + */ + public static function delete_report(int $reportid): bool { + $report = report_model::get_record(['id' => $reportid, 'type' => datasource::TYPE_CUSTOM_REPORT]); + if ($report === false) { + throw new invalid_parameter_exception('Invalid report'); + } + + return $report->delete(); + } +} diff --git a/reportbuilder/classes/local/models/report.php b/reportbuilder/classes/local/models/report.php index 2d0a65d9add..1cc209ffb45 100644 --- a/reportbuilder/classes/local/models/report.php +++ b/reportbuilder/classes/local/models/report.php @@ -21,6 +21,9 @@ namespace core_reportbuilder\local\models; use context; use context_system; use core\persistent; +use core_reportbuilder\event\report_created; +use core_reportbuilder\event\report_deleted; +use core_reportbuilder\event\report_updated; use core_reportbuilder\local\report\base; /** @@ -91,6 +94,56 @@ class report extends persistent { ]; } + /** + * Trigger report created event when persistent is created + */ + protected function after_create(): void { + if ($this->get('type') === base::TYPE_CUSTOM_REPORT) { + report_created::create_from_object($this)->trigger(); + } + } + + /** + * Cascade report deletion, first deleting any linked persistents + */ + protected function before_delete(): void { + $reportparams = ['reportid' => $this->get('id')]; + + // Columns. + foreach (column::get_records($reportparams) as $column) { + $column->delete(); + } + + // Filters. + foreach (filter::get_records($reportparams) as $filter) { + $filter->delete(); + } + } + + /** + * Throw report deleted event when persistent is deleted + * + * @param bool $result + */ + protected function after_delete($result): void { + if (!$result || $this->get('type') === base::TYPE_SYSTEM_REPORT) { + return; + } + report_deleted::create_from_object($this)->trigger(); + } + + /** + * Throw report updated event when persistent is updated + * + * @param bool $result + */ + protected function after_update($result): void { + if (!$result || $this->get('type') === base::TYPE_SYSTEM_REPORT) { + return; + } + report_updated::create_from_object($this)->trigger(); + } + /** * Return report context, used by exporters * diff --git a/reportbuilder/classes/local/report/base.php b/reportbuilder/classes/local/report/base.php index 4ab8a5b3982..5d0eedfd41e 100644 --- a/reportbuilder/classes/local/report/base.php +++ b/reportbuilder/classes/local/report/base.php @@ -119,13 +119,6 @@ abstract class base { */ abstract protected function initialise(): void; - /** - * Output the report - * - * @return string - */ - abstract public function output(): string; - /** * Get the report availability. Sub-classes should override this method to declare themselves unavailable, for example if * they require classes that aren't present due to missing plugin @@ -258,6 +251,21 @@ abstract class base { $this->entities[$entityname] = $entity->initialise(); } + /** + * Returns the entity added to the report from the given entity name + * + * @param string $name + * @return entity_base + * @throws coding_exception + */ + final protected function get_entity(string $name): entity_base { + if (!array_key_exists($name, $this->entities)) { + throw new coding_exception('Invalid entity name', $name); + } + + return $this->entities[$name]; + } + /** * Define a new entity for the report * @@ -300,6 +308,20 @@ abstract class base { return $column; } + /** + * Add given column to the report from an entity + * + * The entity must have already been added to the report before calling this method + * + * @param string $uniqueidentifier + * @return column + */ + final protected function add_column_from_entity(string $uniqueidentifier): column { + [$entityname, $columnname] = explode(':', $uniqueidentifier, 2); + + return $this->add_column($this->get_entity($entityname)->get_column($columnname)); + } + /** * Add given columns to the report from one or more entities * diff --git a/reportbuilder/classes/local/systemreports/reports_list.php b/reportbuilder/classes/local/systemreports/reports_list.php new file mode 100644 index 00000000000..1bc196b48fb --- /dev/null +++ b/reportbuilder/classes/local/systemreports/reports_list.php @@ -0,0 +1,276 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\local\systemreports; + +use html_writer; +use lang_string; +use moodle_url; +use pix_icon; +use stdClass; +use core_reportbuilder\datasource; +use core_reportbuilder\manager; +use core_reportbuilder\system_report; +use core_reportbuilder\local\entities\user; +use core_reportbuilder\local\filters\date; +use core_reportbuilder\local\filters\text; +use core_reportbuilder\local\filters\select; +use core_reportbuilder\local\helpers\format; +use core_reportbuilder\local\report\action; +use core_reportbuilder\local\report\column; +use core_reportbuilder\local\report\filter; +use core_reportbuilder\output\report_name_editable; +use core_reportbuilder\local\models\report; +use core_reportbuilder\permission; + +/** + * Reports list + * + * @package core_reportbuilder + * @copyright 2021 David Matamoros + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class reports_list extends system_report { + + /** + * The name of our internal report entity + * + * @return string + */ + private function get_report_entity_name(): string { + return 'report'; + } + + /** + * Initialise the report + */ + protected function initialise(): void { + $this->set_main_table('reportbuilder_report', 'rb'); + + $this->add_base_condition_simple('rb.type', self::TYPE_CUSTOM_REPORT); + $this->add_base_fields('rb.id, rb.name, rb.source, rb.type, rb.usercreated'); // Necessary for actions/row class. + + // Join user entity for "User modified" column. + $entityuser = new user(); + $entityuseralias = $entityuser->get_table_alias('user'); + + $this->add_entity($entityuser + ->add_join("JOIN {user} {$entityuseralias} ON {$entityuseralias}.id = rb.usermodified") + ); + + // Define our internal entity for report elements. + $this->annotate_entity($this->get_report_entity_name(), + new lang_string('customreports', 'core_reportbuilder')); + + $this->add_columns(); + $this->add_filters(); + $this->add_actions(); + + $this->set_downloadable(false); + } + + /** + * Ensure we can view the report + * + * @return bool + */ + protected function can_view(): bool { + return permission::can_view_reports_list(); + } + + /** + * Dim the table row for invalid datasource + * + * @param stdClass $row + * @return string + */ + public function get_row_class(stdClass $row): string { + return $this->report_source_valid($row->source) ? '' : 'dimmed_text'; + } + + /** + * Add columns to report + */ + protected function add_columns(): void { + $tablealias = $this->get_main_table_alias(); + + // Report name column. + $this->add_column((new column( + 'name', + new lang_string('name'), + $this->get_report_entity_name() + )) + ->set_type(column::TYPE_TEXT) + ->add_fields("{$tablealias}.name, {$tablealias}.id") + ->set_is_sortable(true) + ->add_callback(function(string $value, stdClass $row) { + global $PAGE; + + $reportid = (int) $row->id; + + $editable = new report_name_editable($reportid); + return $editable->render($PAGE->get_renderer('core')); + }) + ); + + // Report source column. + $this->add_column((new column( + 'source', + new lang_string('reportsource', 'core_reportbuilder'), + $this->get_report_entity_name() + )) + ->set_type(column::TYPE_TEXT) + ->add_fields("{$tablealias}.source") + ->set_is_sortable(true) + ->add_callback(function(string $value, stdClass $row) { + if (!$this->report_source_valid($value)) { + // Add danger badge if report source is not valid (either it's missing, or has errors). + return html_writer::span(get_string('errorsourceinvalid', 'core_reportbuilder'), 'badge badge-danger'); + } + + return call_user_func([$value, 'get_name']); + }) + ); + + // Time created column. + $this->add_column((new column( + 'timecreated', + new lang_string('timecreated', 'core_reportbuilder'), + $this->get_report_entity_name() + )) + ->set_type(column::TYPE_TIMESTAMP) + ->add_fields("{$tablealias}.timecreated") + ->set_is_sortable(true) + ->add_callback([format::class, 'userdate']) + ); + + // Time modified column. + $this->add_column((new column( + 'timemodified', + new lang_string('timemodified', 'core_reportbuilder'), + $this->get_report_entity_name() + )) + ->set_type(column::TYPE_TIMESTAMP) + ->add_fields("{$tablealias}.timemodified") + ->set_is_sortable(true) + ->add_callback([format::class, 'userdate']) + ); + + // The user who modified the report. + $this->add_column_from_entity('user:fullname'); + + // Initial sorting. + $this->set_initial_sort_column('report:timecreated', SORT_DESC); + } + + /** + * Add filters to report + */ + protected function add_filters(): void { + $tablealias = $this->get_main_table_alias(); + + // Name filter. + $this->add_filter((new filter( + text::class, + 'name', + new lang_string('name'), + $this->get_report_entity_name(), + "{$tablealias}.name" + ))); + + // Source filter. + $this->add_filter((new filter( + select::class, + 'source', + new lang_string('reportsource', 'core_reportbuilder'), + $this->get_report_entity_name(), + "{$tablealias}.source" + )) + ->set_options_callback(static function(): array { + return manager::get_report_datasources(); + }) + ); + + // Time created filter. + $this->add_filter((new filter( + date::class, + 'timecreated', + new lang_string('timecreated', 'core_reportbuilder'), + $this->get_report_entity_name(), + "{$tablealias}.timecreated" + )) + ->set_limited_operators([ + date::DATE_ANY, + date::DATE_RANGE, + ]) + ); + } + + /** + * Add actions to report + */ + protected function add_actions(): void { + $this->add_action((new action( + new moodle_url('#'), + new pix_icon('t/edit', get_string('editreportdetails', 'core_reportbuilder')), + ['data-action' => 'report-edit', 'data-report-id' => ':id'] + )) + ->add_callback(function(stdClass $row): bool { + return $this->report_source_valid($row->source) && permission::can_edit_report($this->get_report_from_row($row)); + }) + ); + + $this->add_action((new action( + new moodle_url('#'), + new pix_icon('t/delete', get_string('deletereport', 'core_reportbuilder')), + ['data-action' => 'report-delete', 'data-report-id' => ':id', 'data-report-name' => ':name'] + )) + ->add_callback(function(stdClass $row): bool { + // We don't check whether report is valid to ensure editor can always delete them. + return permission::can_edit_report($this->get_report_from_row($row)); + }) + ); + } + + /** + * Helper to determine whether given report source is valid (it both exists, and is available) + * + * @param string $source + * @return bool + */ + private function report_source_valid(string $source): bool { + return manager::report_source_exists($source, datasource::class) && manager::report_source_available($source); + } + + /** + * Helper to return the report persistent from the row object. + * + * Note that this persistent, for performance reasons, is not complete and only contains id/type/usercreated fields, which + * are needed for the permission methods. + * + * @param stdClass $row + * @return report + */ + private function get_report_from_row(stdClass $row): report { + return new report(0, (object)[ + 'id' => $row->id, + 'type' => $row->type, + 'usercreated' => $row->usercreated, + ]); + } +} diff --git a/reportbuilder/classes/manager.php b/reportbuilder/classes/manager.php index 550e7f3153a..015cc3f948f 100644 --- a/reportbuilder/classes/manager.php +++ b/reportbuilder/classes/manager.php @@ -18,6 +18,8 @@ declare(strict_types=1); namespace core_reportbuilder; +use core_component; +use core_plugin_manager; use stdClass; use core_reportbuilder\local\models\report; use core_reportbuilder\local\report\base; @@ -53,6 +55,19 @@ class manager { return new $source($report, $parameters); } + /** + * Return an instance of a report class from the given report ID + * + * @param int $reportid + * @param array $parameters + * @return base + */ + public static function get_report_from_id(int $reportid, array $parameters = []): base { + $report = new report($reportid); + + return self::get_report_from_persistent($report, $parameters); + } + /** * Verify that report source exists and extends appropriate base classes * @@ -84,4 +99,30 @@ class manager { public static function create_report_persistent(stdClass $reportdata): report { return (new report(0, $reportdata))->create(); } + + /** + * Return an array of all valid report sources across the site + * + * @return array[][] Indexed by [component => [class => name]] + */ + public static function get_report_datasources(): array { + $sources = array(); + + $datasources = core_component::get_component_classes_in_namespace(null, 'reportbuilder\\datasource'); + foreach ($datasources as $class => $path) { + if (self::report_source_exists($class, datasource::class) && self::report_source_available($class)) { + [$component] = explode('\\', $class); + + if ($plugininfo = core_plugin_manager::instance()->get_plugin_info($component)) { + $componentname = $plugininfo->displayname; + } else { + $componentname = get_string('site'); + } + + $sources[$componentname][$class] = call_user_func([$class, 'get_name']); + } + } + + return $sources; + } } diff --git a/reportbuilder/classes/output/renderer.php b/reportbuilder/classes/output/renderer.php index b9cdb45873f..c184d9ff54e 100644 --- a/reportbuilder/classes/output/renderer.php +++ b/reportbuilder/classes/output/renderer.php @@ -18,6 +18,7 @@ declare(strict_types=1); namespace core_reportbuilder\output; +use html_writer; use plugin_renderer_base; use core_reportbuilder\table\system_report_table; @@ -56,4 +57,16 @@ class renderer extends plugin_renderer_base { return $output; } + + /** + * Renders the New report button + * + * @return string + */ + public function render_new_report_button(): string { + return html_writer::tag('button', get_string('newreport', 'core_reportbuilder'), [ + 'class' => 'btn btn-primary my-auto', + 'data-action' => 'report-create', + ]); + } } diff --git a/reportbuilder/classes/output/report_name_editable.php b/reportbuilder/classes/output/report_name_editable.php new file mode 100644 index 00000000000..93f6889c839 --- /dev/null +++ b/reportbuilder/classes/output/report_name_editable.php @@ -0,0 +1,82 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\output; + +use core_external; +use html_writer; +use moodle_url; +use core\output\inplace_editable; +use core_reportbuilder\permission; +use core_reportbuilder\local\models\report; + +/** + * Report name editable component + * + * @package core_reportbuilder + * @copyright 2021 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class report_name_editable extends inplace_editable { + + /** + * Class constructor + * + * @param int $reportid + * @param report|null $report + */ + public function __construct(int $reportid, ?report $report = null) { + if ($report === null) { + $report = new report($reportid); + } + + $editable = permission::can_edit_report($report); + + $url = $editable + ? new moodle_url('/reportbuilder/edit.php', ['id' => $report->get('id')]) + : new moodle_url('/reportbuilder/view.php', ['id' => $report->get('id')]); + + $displayvalue = html_writer::link($url, $report->get_formatted_name()); + + parent::__construct('core_reportbuilder', 'reportname', $report->get('id'), $editable, $displayvalue, $report->get('name'), + get_string('editreportname', 'core_reportbuilder')); + } + + /** + * Update report persistent and return self, called from inplace_editable callback + * + * @param int $reportid + * @param string $value + * @return self + */ + public static function update(int $reportid, string $value): self { + $report = new report($reportid); + + core_external::validate_context($report->get_context()); + permission::require_can_edit_report($report); + + $value = clean_param($value, PARAM_TEXT); + if ($value !== '') { + $report + ->set('name', $value) + ->update(); + } + + return new self(0, $report); + } +} diff --git a/reportbuilder/classes/permission.php b/reportbuilder/classes/permission.php new file mode 100644 index 00000000000..822c454f547 --- /dev/null +++ b/reportbuilder/classes/permission.php @@ -0,0 +1,115 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder; + +use context_system; +use core_reportbuilder\local\models\report; +use core_reportbuilder\local\report\base; + +/** + * Report permission class + * + * @package core_reportbuilder + * @copyright 2021 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class permission { + + /** + * Require given user can view reports list + * + * @param int|null $userid User ID to check, or the current user if omitted + * @throws report_access_exception + */ + public static function require_can_view_reports_list(?int $userid = null): void { + if (!static::can_view_reports_list($userid)) { + throw new report_access_exception(); + } + } + + /** + * Whether given user can view reports list + * + * @param int|null $userid User ID to check, or the current user if omitted + * @return bool + */ + public static function can_view_reports_list(?int $userid = null): bool { + return has_capability('moodle/reportbuilder:view', context_system::instance(), $userid); + } + + /** + * Require given user can edit report + * + * @param report $report + * @param int|null $userid User ID to check, or the current user if omitted + * @return void + * @throws report_access_exception + */ + public static function require_can_edit_report(report $report, ?int $userid = null): void { + if (!static::can_edit_report($report, $userid)) { + throw new report_access_exception('errorreportedit'); + } + } + + /** + * Whether given user can edit report + * + * @param report $report + * @param int|null $userid User ID to check, or the current user if omitted + * @return bool + */ + public static function can_edit_report(report $report, ?int $userid = null): bool { + global $USER; + + // We can only edit custom reports. + if ($report->get('type') !== base::TYPE_CUSTOM_REPORT) { + return false; + } + + $userid = $userid ?: (int) $USER->id; + if ($report->get('usercreated') === $userid) { + return has_capability('moodle/reportbuilder:edit', context_system::instance(), $userid); + } else { + return has_capability('moodle/reportbuilder:editall', context_system::instance(), $userid); + } + } + + /** + * Whether given user can create a new report + * + * @param int|null $userid User ID to check, or the current user if omitted + * @return bool + */ + public static function can_create_report(?int $userid = null): bool { + $capabilities = ['moodle/reportbuilder:edit', 'moodle/reportbuilder:editall']; + return has_any_capability($capabilities, context_system::instance(), $userid); + } + + /** + * Require given user can create a new report + * + * @param int|null $userid User ID to check, or the current user if omitted + * @throws report_access_exception + */ + public static function require_can_create_report(?int $userid = null): void { + if (!static::can_create_report($userid)) { + throw new report_access_exception('errorreportcreate'); + } + } +} diff --git a/reportbuilder/index.php b/reportbuilder/index.php new file mode 100644 index 00000000000..25e18aec0df --- /dev/null +++ b/reportbuilder/index.php @@ -0,0 +1,53 @@ +. + +/** + * List of custom reports + * + * @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\permission; +use core_reportbuilder\system_report_factory; +use core_reportbuilder\local\systemreports\reports_list; + +require_once(__DIR__ . '/../config.php'); +require_once("{$CFG->libdir}/adminlib.php"); + +admin_externalpage_setup('customreports'); + +$PAGE->requires->js_call_amd('core_reportbuilder/reports_list', 'init'); + +echo $OUTPUT->header(); +echo html_writer::start_div('d-flex justify-content-between mb-2'); +echo $OUTPUT->heading(get_string('customreports', 'core_reportbuilder')); + +if (permission::can_create_report()) { + /** @var \core_reportbuilder\output\renderer $renderer */ + $renderer = $PAGE->get_renderer('core_reportbuilder'); + echo $renderer->render_new_report_button(); +} + +echo html_writer::end_div(); + +$report = system_report_factory::create(reports_list::class, context_system::instance()); +echo $report->output(); + +echo $OUTPUT->footer(); diff --git a/reportbuilder/lib.php b/reportbuilder/lib.php index 628e1bf302f..06b50f453e5 100644 --- a/reportbuilder/lib.php +++ b/reportbuilder/lib.php @@ -42,3 +42,23 @@ function core_reportbuilder_output_fragment_filters_form(array $params): string return $filtersform->render(); } + +/** + * Plugin inplace editable implementation + * + * @param string $itemtype + * @param int $itemid + * @param string $newvalue + * @return \core\output\inplace_editable|bool + */ +function core_reportbuilder_inplace_editable($itemtype, $itemid, $newvalue) { + $itemid = (int) $itemid; + + switch ($itemtype) { + case 'reportname': + return \core_reportbuilder\output\report_name_editable::update($itemid, $newvalue); + + default: + return false; + } +} diff --git a/user/classes/reportbuilder/datasource/users.php b/user/classes/reportbuilder/datasource/users.php new file mode 100644 index 00000000000..a8bd9604edf --- /dev/null +++ b/user/classes/reportbuilder/datasource/users.php @@ -0,0 +1,62 @@ +. + +declare(strict_types=1); + +namespace core_user\reportbuilder\datasource; + +use core_reportbuilder\datasource; +use core_reportbuilder\local\entities\user; +use core_reportbuilder\local\helpers\database; + +/** + * Users datasource + * + * @package core_reportbuilder + * @copyright 2021 David Matamoros + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class users extends datasource { + + /** + * Return user friendly name of the datasource + * + * @return string + */ + public static function get_name(): string { + return get_string('users'); + } + + /** + * Initialise report + */ + protected function initialise(): void { + global $CFG; + + $userentity = new user(); + $usertablealias = $userentity->get_table_alias('user'); + + $this->set_main_table('user', $usertablealias); + + $userparamguest = database::generate_param_name(); + $this->add_base_condition_sql("{$usertablealias}.id != :{$userparamguest} AND {$usertablealias}.deleted = 0", [ + $userparamguest => $CFG->siteguest, + ]); + + // Add all columns from entities to be available in custom reports. + $this->add_entity($userentity); + } +}