diff --git a/.upgradenotes/MDL-86066-2025082110031090.yml b/.upgradenotes/MDL-86066-2025082110031090.yml new file mode 100644 index 00000000000..1a1ef4f9b27 --- /dev/null +++ b/.upgradenotes/MDL-86066-2025082110031090.yml @@ -0,0 +1,19 @@ +issueNumber: MDL-86066 +notes: + core_reportbuilder: + - message: > + The following methods from the `schedule` helper class have been + deprecated, in favour of usage of the new schedule type system: + + + * `create_schedule` + + * `get_report_empty_options` + + * `send_schedule_message` + type: deprecated + - message: >- + Report schedule types are now extendable by third-party plugins by + extending the `core_reportbuilder\local\schedules\base` class in your + component namespace: `\reportbuilder\schedule\` + type: improved diff --git a/public/lang/en/deprecated.txt b/public/lang/en/deprecated.txt index b88a88c5071..a857c67f778 100644 --- a/public/lang/en/deprecated.txt +++ b/public/lang/en/deprecated.txt @@ -89,3 +89,6 @@ resources_help,core unabletomessage,core_message notificationimage,core_message hiddensections_help,core +privacy:metadata:schedule:message,core_reportbuilder +privacy:metadata:schedule:reportempty,core_reportbuilder +privacy:metadata:schedule:subject,core_reportbuilder diff --git a/public/lang/en/reportbuilder.php b/public/lang/en/reportbuilder.php index d2f21096ada..92a36b5ef48 100644 --- a/public/lang/en/reportbuilder.php +++ b/public/lang/en/reportbuilder.php @@ -214,13 +214,12 @@ $string['privacy:metadata:report:usercreated'] = 'The ID of the user who created $string['privacy:metadata:report:usermodified'] = 'The ID of the user who last modified the report'; $string['privacy:metadata:schedule'] = 'Report schedule definitions'; $string['privacy:metadata:schedule:audiences'] = 'The audiences this schedule is for'; +$string['privacy:metadata:schedule:classname'] = 'The class used by the schedule'; +$string['privacy:metadata:schedule:configdata'] = 'Configuration data used by the schedule'; $string['privacy:metadata:schedule:enabled'] = 'The status of the schedule'; $string['privacy:metadata:schedule:format'] = 'The format of the scheduled report'; -$string['privacy:metadata:schedule:message'] = 'The message of the schedule'; $string['privacy:metadata:schedule:name'] = 'The name of the schedule'; $string['privacy:metadata:schedule:recurrence'] = 'The recurrence of the schedule'; -$string['privacy:metadata:schedule:reportempty'] = 'Action to take if scheduled report is empty'; -$string['privacy:metadata:schedule:subject'] = 'The subject of the schedule'; $string['privacy:metadata:schedule:timecreated'] = 'The time when the schedule was created'; $string['privacy:metadata:schedule:timemodified'] = 'The time when the schedule was last modified'; $string['privacy:metadata:schedule:timescheduled'] = 'The time the schedule will begin'; @@ -255,6 +254,8 @@ $string['resetconditions'] = 'Reset conditions'; $string['resetconditionsconfirm'] = 'Are you sure you want to reset all conditions for this report?'; $string['schedulecreated'] = 'Schedule created'; $string['scheduledeleted'] = 'Schedule deleted'; +$string['scheduleemail'] = 'Schedule an email'; +$string['scheduleemaildescription'] = 'Set up a recurring email to share this report with your chosen audience'; $string['scheduleempty'] = 'If the report is empty'; $string['scheduleemptydontsend'] = 'Don\'t send message'; $string['scheduleemptysendwithattachment'] = 'Send message with empty report'; @@ -302,3 +303,8 @@ $string['filterdateto'] = 'Date to'; // Deprecated since Moodle 5.0. $string['privacy:metadata:preference:reportfilter'] = 'Stored report filter values'; $string['userpicture'] = 'User picture'; + +// Deprecated since Moodle 5.1. +$string['privacy:metadata:schedule:message'] = 'The message of the schedule'; +$string['privacy:metadata:schedule:reportempty'] = 'Action to take if scheduled report is empty'; +$string['privacy:metadata:schedule:subject'] = 'The subject of the schedule'; diff --git a/public/lib/db/install.xml b/public/lib/db/install.xml index dbc8c6bf639..b4d070ad7e7 100644 --- a/public/lib/db/install.xml +++ b/public/lib/db/install.xml @@ -4669,15 +4669,13 @@ - + + + - - - - diff --git a/public/lib/db/upgrade.php b/public/lib/db/upgrade.php index 8421206ef15..580382d7782 100644 --- a/public/lib/db/upgrade.php +++ b/public/lib/db/upgrade.php @@ -2125,5 +2125,70 @@ function xmldb_main_upgrade($oldversion) { upgrade_main_savepoint(true, 2025090200.01); } + if ($oldversion < 2025090200.02) { + $table = new xmldb_table('reportbuilder_schedule'); + + // Conditionally launch add field classname. + $field = new xmldb_field('classname', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null, 'enabled'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Conditionally launch add field configdata. + $field = new xmldb_field('configdata', XMLDB_TYPE_TEXT, null, null, null, null, null, 'classname'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Migrate existing data to new structure. + $schedules = $DB->get_records('reportbuilder_schedule'); + foreach ($schedules as $schedule) { + $DB->update_record('reportbuilder_schedule', [ + 'id' => $schedule->id, + 'classname' => core_reportbuilder\reportbuilder\schedule\message::class, + 'configdata' => json_encode([ + 'subject' => $schedule->subject, + 'message' => ['text' => $schedule->message, 'format' => $schedule->messageformat], + 'reportempty' => $schedule->reportempty, + ]), + ]); + } + + // Launch change of nullability for field configdata (after migrating data). + $field = new xmldb_field('configdata', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null, 'classname'); + $dbman->change_field_notnull($table, $field); + + // Launch change of nullability for field audiences. + $field = new xmldb_field('audiences', XMLDB_TYPE_TEXT, null, null, null, null, null, 'enabled'); + $dbman->change_field_notnull($table, $field); + + // Conditionally launch drop field subject. + $field = new xmldb_field('subject'); + if ($dbman->field_exists($table, $field)) { + $dbman->drop_field($table, $field); + } + + // Conditionally launch drop field message. + $field = new xmldb_field('message'); + if ($dbman->field_exists($table, $field)) { + $dbman->drop_field($table, $field); + } + + // Conditionally launch drop field messageformat. + $field = new xmldb_field('messageformat'); + if ($dbman->field_exists($table, $field)) { + $dbman->drop_field($table, $field); + } + + // Conditionally launch drop field reportempty. + $field = new xmldb_field('reportempty'); + if ($dbman->field_exists($table, $field)) { + $dbman->drop_field($table, $field); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2025090200.02); + } + return true; } diff --git a/public/reportbuilder/amd/build/local/repository/modals.min.js b/public/reportbuilder/amd/build/local/repository/modals.min.js index 5eb39147a01..cf74d543969 100644 --- a/public/reportbuilder/amd/build/local/repository/modals.min.js +++ b/public/reportbuilder/amd/build/local/repository/modals.min.js @@ -5,6 +5,6 @@ define("core_reportbuilder/local/repository/modals",["exports","core_form/modalf * @module core_reportbuilder/local/repository/modals * @copyright 2021 David Matamoros * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.createScheduleModal=_exports.createReportModal=_exports.createDuplicateReportModal=void 0,_modalform=(obj=_modalform)&&obj.__esModule?obj:{default:obj};const createModalForm=(triggerElement,modalTitle,formClass,formArgs)=>new _modalform.default({modalConfig:{title:modalTitle},formClass:formClass,args:formArgs,saveButtonText:(0,_str.getString)("save","moodle"),returnFocus:triggerElement});_exports.createReportModal=function(triggerElement,modalTitle){let reportId=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0;return createModalForm(triggerElement,modalTitle,"core_reportbuilder\\form\\report",{id:reportId})};_exports.createDuplicateReportModal=(triggerElement,modalTitle,reportId,reportName)=>createModalForm(triggerElement,modalTitle,"core_reportbuilder\\form\\duplicate_report",{id:reportId,name:reportName});_exports.createScheduleModal=function(triggerElement,modalTitle,reportId){let scheduleId=arguments.length>3&&void 0!==arguments[3]?arguments[3]:0;return createModalForm(triggerElement,modalTitle,"core_reportbuilder\\form\\schedule",{reportid:reportId,id:scheduleId})}})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.createScheduleModal=_exports.createReportModal=_exports.createDuplicateReportModal=void 0,_modalform=(obj=_modalform)&&obj.__esModule?obj:{default:obj};const createModalForm=(triggerElement,modalTitle,formClass,formArgs)=>new _modalform.default({modalConfig:{title:modalTitle},formClass:formClass,args:formArgs,saveButtonText:(0,_str.getString)("save","moodle"),returnFocus:triggerElement});_exports.createReportModal=function(triggerElement,modalTitle){let reportId=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0;return createModalForm(triggerElement,modalTitle,"core_reportbuilder\\form\\report",{id:reportId})};_exports.createDuplicateReportModal=(triggerElement,modalTitle,reportId,reportName)=>createModalForm(triggerElement,modalTitle,"core_reportbuilder\\form\\duplicate_report",{id:reportId,name:reportName});_exports.createScheduleModal=function(triggerElement,modalTitle,reportId){let scheduleId=arguments.length>3&&void 0!==arguments[3]?arguments[3]:0,scheduleClass=arguments.length>4&&void 0!==arguments[4]?arguments[4]:"";return createModalForm(triggerElement,modalTitle,"core_reportbuilder\\form\\schedule",{reportid:reportId,id:scheduleId,classname:scheduleClass})}})); //# sourceMappingURL=modals.min.js.map \ No newline at end of file diff --git a/public/reportbuilder/amd/build/local/repository/modals.min.js.map b/public/reportbuilder/amd/build/local/repository/modals.min.js.map index 77228f201e7..7b796ffe0fe 100644 --- a/public/reportbuilder/amd/build/local/repository/modals.min.js.map +++ b/public/reportbuilder/amd/build/local/repository/modals.min.js.map @@ -1 +1 @@ -{"version":3,"file":"modals.min.js","sources":["../../../src/local/repository/modals.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Module to handle modal form requests\n *\n * @module core_reportbuilder/local/repository/modals\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 {getString} from 'core/str';\n\n/**\n * Return modal instance\n *\n * @param {EventTarget} triggerElement\n * @param {Promise} modalTitle\n * @param {String} formClass\n * @param {Object} formArgs\n * @return {ModalForm}\n */\nconst createModalForm = (triggerElement, modalTitle, formClass, formArgs) => {\n return new ModalForm({\n modalConfig: {\n title: modalTitle,\n },\n formClass: formClass,\n args: formArgs,\n saveButtonText: getString('save', 'moodle'),\n returnFocus: triggerElement,\n });\n};\n\n/**\n * Return report modal instance\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 createModalForm(triggerElement, modalTitle, 'core_reportbuilder\\\\form\\\\report', {\n id: reportId,\n });\n};\n\n/**\n * Return duplicate report modal instance\n *\n * @param {EventTarget} triggerElement\n * @param {Promise} modalTitle\n * @param {Number} reportId\n * @param {String} reportName\n * @return {ModalForm}\n */\nexport const createDuplicateReportModal = (triggerElement, modalTitle, reportId, reportName) => {\n return createModalForm(triggerElement, modalTitle, 'core_reportbuilder\\\\form\\\\duplicate_report', {\n id: reportId,\n name: reportName,\n });\n};\n\n/**\n * Return schedule modal instance\n *\n * @param {EventTarget} triggerElement\n * @param {Promise} modalTitle\n * @param {Number} reportId\n * @param {Number} scheduleId\n * @return {ModalForm}\n */\nexport const createScheduleModal = (triggerElement, modalTitle, reportId, scheduleId = 0) => {\n return createModalForm(triggerElement, modalTitle, 'core_reportbuilder\\\\form\\\\schedule', {\n reportid: reportId,\n id: scheduleId,\n });\n};\n"],"names":["createModalForm","triggerElement","modalTitle","formClass","formArgs","ModalForm","modalConfig","title","args","saveButtonText","returnFocus","reportId","id","reportName","name","scheduleId","reportid"],"mappings":";;;;;;;oOAmCMA,gBAAkB,CAACC,eAAgBC,WAAYC,UAAWC,WACrD,IAAIC,mBAAU,CACjBC,YAAa,CACTC,MAAOL,YAEXC,UAAWA,UACXK,KAAMJ,SACNK,gBAAgB,kBAAU,OAAQ,UAClCC,YAAaT,4CAYY,SAACA,eAAgBC,gBAAYS,gEAAW,SAC9DX,gBAAgBC,eAAgBC,WAAY,mCAAoC,CACnFU,GAAID,gDAa8B,CAACV,eAAgBC,WAAYS,SAAUE,aACtEb,gBAAgBC,eAAgBC,WAAY,6CAA8C,CAC7FU,GAAID,SACJG,KAAMD,0CAaqB,SAACZ,eAAgBC,WAAYS,cAAUI,kEAAa,SAC5Ef,gBAAgBC,eAAgBC,WAAY,qCAAsC,CACrFc,SAAUL,SACVC,GAAIG"} \ No newline at end of file +{"version":3,"file":"modals.min.js","sources":["../../../src/local/repository/modals.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Module to handle modal form requests\n *\n * @module core_reportbuilder/local/repository/modals\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 {getString} from 'core/str';\n\n/**\n * Return modal instance\n *\n * @param {EventTarget} triggerElement\n * @param {Promise} modalTitle\n * @param {String} formClass\n * @param {Object} formArgs\n * @return {ModalForm}\n */\nconst createModalForm = (triggerElement, modalTitle, formClass, formArgs) => {\n return new ModalForm({\n modalConfig: {\n title: modalTitle,\n },\n formClass: formClass,\n args: formArgs,\n saveButtonText: getString('save', 'moodle'),\n returnFocus: triggerElement,\n });\n};\n\n/**\n * Return report modal instance\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 createModalForm(triggerElement, modalTitle, 'core_reportbuilder\\\\form\\\\report', {\n id: reportId,\n });\n};\n\n/**\n * Return duplicate report modal instance\n *\n * @param {EventTarget} triggerElement\n * @param {Promise} modalTitle\n * @param {Number} reportId\n * @param {String} reportName\n * @return {ModalForm}\n */\nexport const createDuplicateReportModal = (triggerElement, modalTitle, reportId, reportName) => {\n return createModalForm(triggerElement, modalTitle, 'core_reportbuilder\\\\form\\\\duplicate_report', {\n id: reportId,\n name: reportName,\n });\n};\n\n/**\n * Return schedule modal instance\n *\n * @param {EventTarget} triggerElement\n * @param {Promise} modalTitle\n * @param {Number} reportId\n * @param {Number} scheduleId\n * @param {String} scheduleClass\n * @return {ModalForm}\n */\nexport const createScheduleModal = (triggerElement, modalTitle, reportId, scheduleId = 0, scheduleClass = '') => {\n return createModalForm(triggerElement, modalTitle, 'core_reportbuilder\\\\form\\\\schedule', {\n reportid: reportId,\n id: scheduleId,\n classname: scheduleClass,\n });\n};\n"],"names":["createModalForm","triggerElement","modalTitle","formClass","formArgs","ModalForm","modalConfig","title","args","saveButtonText","returnFocus","reportId","id","reportName","name","scheduleId","scheduleClass","reportid","classname"],"mappings":";;;;;;;oOAmCMA,gBAAkB,CAACC,eAAgBC,WAAYC,UAAWC,WACrD,IAAIC,mBAAU,CACjBC,YAAa,CACTC,MAAOL,YAEXC,UAAWA,UACXK,KAAMJ,SACNK,gBAAgB,kBAAU,OAAQ,UAClCC,YAAaT,4CAYY,SAACA,eAAgBC,gBAAYS,gEAAW,SAC9DX,gBAAgBC,eAAgBC,WAAY,mCAAoC,CACnFU,GAAID,gDAa8B,CAACV,eAAgBC,WAAYS,SAAUE,aACtEb,gBAAgBC,eAAgBC,WAAY,6CAA8C,CAC7FU,GAAID,SACJG,KAAMD,0CAcqB,SAACZ,eAAgBC,WAAYS,cAAUI,kEAAa,EAAGC,qEAAgB,UAC/FhB,gBAAgBC,eAAgBC,WAAY,qCAAsC,CACrFe,SAAUN,SACVC,GAAIG,WACJG,UAAWF"} \ No newline at end of file diff --git a/public/reportbuilder/amd/build/schedules.min.js b/public/reportbuilder/amd/build/schedules.min.js index 73bbf3a06ea..9b8275696a7 100644 --- a/public/reportbuilder/amd/build/schedules.min.js +++ b/public/reportbuilder/amd/build/schedules.min.js @@ -1,3 +1,3 @@ -define("core_reportbuilder/schedules",["exports","core/event_dispatcher","core/inplace_editable","core/notification","core/pending","core/prefetch","core/str","core/toast","core_reportbuilder/local/events","core_reportbuilder/local/selectors","core_reportbuilder/local/repository/modals","core_reportbuilder/local/repository/schedules"],(function(_exports,_event_dispatcher,_inplace_editable,_notification,_pending,_prefetch,_str,_toast,reportEvents,reportSelectors,_modals,_schedules){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending),reportEvents=_interopRequireWildcard(reportEvents),reportSelectors=_interopRequireWildcard(reportSelectors);let initialized=!1;_exports.init=reportId=>{(0,_prefetch.prefetchStrings)("core_reportbuilder",["deleteschedule","deletescheduleconfirm","disableschedule","editscheduledetails","enableschedule","newschedule","schedulecreated","scheduledeleted","schedulesent","scheduleupdated","sendschedule","sendscheduleconfirm"]),(0,_prefetch.prefetchStrings)("core",["confirm","delete"]),initialized||(document.addEventListener("click",(event=>{if(event.target.closest(reportSelectors.actions.scheduleCreate)){event.preventDefault();const scheduleModal=(0,_modals.createScheduleModal)(event.target,(0,_str.getString)("newschedule","core_reportbuilder"),reportId);scheduleModal.addEventListener(scheduleModal.events.FORM_SUBMITTED,(()=>{(0,_str.getString)("schedulecreated","core_reportbuilder").then(_toast.add).then((()=>{const reportElement=document.querySelector(reportSelectors.regions.report);(0,_event_dispatcher.dispatchEvent)(reportEvents.tableReload,{},reportElement)})).catch(_notification.default.exception)})),scheduleModal.show()}const scheduleToggle=event.target.closest(reportSelectors.actions.scheduleToggle);if(scheduleToggle){const pendingPromise=new _pending.default("core_reportbuilder/schedules:toggle"),scheduleStateToggle=+!Number(scheduleToggle.dataset.state);(0,_schedules.toggleSchedule)(reportId,scheduleToggle.dataset.id,scheduleStateToggle).then((()=>{scheduleToggle.closest("tr").classList.toggle("text-muted"),scheduleToggle.dataset.state=scheduleStateToggle;const stringKey=scheduleStateToggle?"disableschedule":"enableschedule";return(0,_str.getString)(stringKey,"core_reportbuilder")})).then((toggleLabel=>(scheduleToggle.parentElement.querySelector('label[for="'.concat(scheduleToggle.id,'"] > span')).innerHTML=toggleLabel,pendingPromise.resolve()))).catch(_notification.default.exception)}const scheduleEdit=event.target.closest(reportSelectors.actions.scheduleEdit);if(scheduleEdit){event.preventDefault();const triggerElement=scheduleEdit.closest(".dropdown").querySelector(".dropdown-toggle"),scheduleModal=(0,_modals.createScheduleModal)(triggerElement,(0,_str.getString)("editscheduledetails","core_reportbuilder"),reportId,scheduleEdit.dataset.scheduleId);scheduleModal.addEventListener(scheduleModal.events.FORM_SUBMITTED,(()=>{(0,_str.getString)("scheduleupdated","core_reportbuilder").then(_toast.add).then((()=>{const reportElement=scheduleEdit.closest(reportSelectors.regions.report);(0,_event_dispatcher.dispatchEvent)(reportEvents.tableReload,{},reportElement)})).catch(_notification.default.exception)})),scheduleModal.show()}const scheduleSend=event.target.closest(reportSelectors.actions.scheduleSend);if(scheduleSend){event.preventDefault();const triggerElement=scheduleSend.closest(".dropdown").querySelector(".dropdown-toggle");_notification.default.saveCancelPromise((0,_str.getString)("sendschedule","core_reportbuilder"),(0,_str.getString)("sendscheduleconfirm","core_reportbuilder",scheduleSend.dataset.scheduleName),(0,_str.getString)("confirm","core"),{triggerElement:triggerElement}).then((()=>{const pendingPromise=new _pending.default("core_reportbuilder/schedules:send");return(0,_schedules.sendSchedule)(reportId,scheduleSend.dataset.scheduleId).then((0,_toast.add)((0,_str.getString)("schedulesent","core_reportbuilder"))).then((()=>pendingPromise.resolve())).catch(_notification.default.exception)})).catch((()=>{}))}const scheduleDelete=event.target.closest(reportSelectors.actions.scheduleDelete);if(scheduleDelete){event.preventDefault();const triggerElement=scheduleDelete.closest(".dropdown").querySelector(".dropdown-toggle");_notification.default.saveCancelPromise((0,_str.getString)("deleteschedule","core_reportbuilder"),(0,_str.getString)("deletescheduleconfirm","core_reportbuilder",scheduleDelete.dataset.scheduleName),(0,_str.getString)("delete","core"),{triggerElement:triggerElement}).then((()=>{const pendingPromise=new _pending.default("core_reportbuilder/schedules:delete");return(0,_schedules.deleteSchedule)(reportId,scheduleDelete.dataset.scheduleId).then((0,_toast.add)((0,_str.getString)("scheduledeleted","core_reportbuilder"))).then((()=>{const reportElement=scheduleDelete.closest(reportSelectors.regions.report);return(0,_event_dispatcher.dispatchEvent)(reportEvents.tableReload,{preservePagination:!0},reportElement),pendingPromise.resolve()})).catch(_notification.default.exception)})).catch((()=>{}))}})),initialized=!0)}})); +define("core_reportbuilder/schedules",["exports","core/event_dispatcher","core/inplace_editable","core/notification","core/pending","core/prefetch","core/str","core/toast","core_reportbuilder/local/events","core_reportbuilder/local/selectors","core_reportbuilder/local/repository/modals","core_reportbuilder/local/repository/schedules"],(function(_exports,_event_dispatcher,_inplace_editable,_notification,_pending,_prefetch,_str,_toast,reportEvents,reportSelectors,_modals,_schedules){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending),reportEvents=_interopRequireWildcard(reportEvents),reportSelectors=_interopRequireWildcard(reportSelectors);let initialized=!1;_exports.init=reportId=>{(0,_prefetch.prefetchStrings)("core_reportbuilder",["deleteschedule","deletescheduleconfirm","disableschedule","editscheduledetails","enableschedule","newschedule","schedulecreated","scheduledeleted","schedulesent","scheduleupdated","sendschedule","sendscheduleconfirm"]),(0,_prefetch.prefetchStrings)("core",["confirm","delete"]),initialized||(document.addEventListener("click",(event=>{const scheduleCreate=event.target.closest(reportSelectors.actions.scheduleCreate);if(scheduleCreate){event.preventDefault();const{scheduleClass:scheduleClass}=scheduleCreate.dataset,scheduleModal=(0,_modals.createScheduleModal)(event.target,(0,_str.getString)("newschedule","core_reportbuilder"),reportId,0,scheduleClass);scheduleModal.addEventListener(scheduleModal.events.FORM_SUBMITTED,(()=>{(0,_str.getString)("schedulecreated","core_reportbuilder").then(_toast.add).then((()=>{const reportElement=document.querySelector(reportSelectors.regions.report);(0,_event_dispatcher.dispatchEvent)(reportEvents.tableReload,{},reportElement)})).catch(_notification.default.exception)})),scheduleModal.show()}const scheduleToggle=event.target.closest(reportSelectors.actions.scheduleToggle);if(scheduleToggle){const pendingPromise=new _pending.default("core_reportbuilder/schedules:toggle"),scheduleStateToggle=+!Number(scheduleToggle.dataset.state);(0,_schedules.toggleSchedule)(reportId,scheduleToggle.dataset.id,scheduleStateToggle).then((()=>{scheduleToggle.closest("tr").classList.toggle("text-muted"),scheduleToggle.dataset.state=scheduleStateToggle;const stringKey=scheduleStateToggle?"disableschedule":"enableschedule";return(0,_str.getString)(stringKey,"core_reportbuilder")})).then((toggleLabel=>(scheduleToggle.parentElement.querySelector('label[for="'.concat(scheduleToggle.id,'"] > span')).innerHTML=toggleLabel,pendingPromise.resolve()))).catch(_notification.default.exception)}const scheduleEdit=event.target.closest(reportSelectors.actions.scheduleEdit);if(scheduleEdit){event.preventDefault();const triggerElement=scheduleEdit.closest(".dropdown").querySelector(".dropdown-toggle"),scheduleModal=(0,_modals.createScheduleModal)(triggerElement,(0,_str.getString)("editscheduledetails","core_reportbuilder"),reportId,scheduleEdit.dataset.scheduleId);scheduleModal.addEventListener(scheduleModal.events.FORM_SUBMITTED,(()=>{(0,_str.getString)("scheduleupdated","core_reportbuilder").then(_toast.add).then((()=>{const reportElement=scheduleEdit.closest(reportSelectors.regions.report);(0,_event_dispatcher.dispatchEvent)(reportEvents.tableReload,{},reportElement)})).catch(_notification.default.exception)})),scheduleModal.show()}const scheduleSend=event.target.closest(reportSelectors.actions.scheduleSend);if(scheduleSend){event.preventDefault();const triggerElement=scheduleSend.closest(".dropdown").querySelector(".dropdown-toggle");_notification.default.saveCancelPromise((0,_str.getString)("sendschedule","core_reportbuilder"),(0,_str.getString)("sendscheduleconfirm","core_reportbuilder",scheduleSend.dataset.scheduleName),(0,_str.getString)("confirm","core"),{triggerElement:triggerElement}).then((()=>{const pendingPromise=new _pending.default("core_reportbuilder/schedules:send");return(0,_schedules.sendSchedule)(reportId,scheduleSend.dataset.scheduleId).then((0,_toast.add)((0,_str.getString)("schedulesent","core_reportbuilder"))).then((()=>pendingPromise.resolve())).catch(_notification.default.exception)})).catch((()=>{}))}const scheduleDelete=event.target.closest(reportSelectors.actions.scheduleDelete);if(scheduleDelete){event.preventDefault();const triggerElement=scheduleDelete.closest(".dropdown").querySelector(".dropdown-toggle");_notification.default.saveCancelPromise((0,_str.getString)("deleteschedule","core_reportbuilder"),(0,_str.getString)("deletescheduleconfirm","core_reportbuilder",scheduleDelete.dataset.scheduleName),(0,_str.getString)("delete","core"),{triggerElement:triggerElement}).then((()=>{const pendingPromise=new _pending.default("core_reportbuilder/schedules:delete");return(0,_schedules.deleteSchedule)(reportId,scheduleDelete.dataset.scheduleId).then((0,_toast.add)((0,_str.getString)("scheduledeleted","core_reportbuilder"))).then((()=>{const reportElement=scheduleDelete.closest(reportSelectors.regions.report);return(0,_event_dispatcher.dispatchEvent)(reportEvents.tableReload,{preservePagination:!0},reportElement),pendingPromise.resolve()})).catch(_notification.default.exception)})).catch((()=>{}))}})),initialized=!0)}})); //# sourceMappingURL=schedules.min.js.map \ No newline at end of file diff --git a/public/reportbuilder/amd/build/schedules.min.js.map b/public/reportbuilder/amd/build/schedules.min.js.map index a8b823a912a..f4795e098fd 100644 --- a/public/reportbuilder/amd/build/schedules.min.js.map +++ b/public/reportbuilder/amd/build/schedules.min.js.map @@ -1 +1 @@ -{"version":3,"file":"schedules.min.js","sources":["../src/schedules.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Report builder audiences\n *\n * @module core_reportbuilder/schedules\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 {dispatchEvent} from 'core/event_dispatcher';\nimport 'core/inplace_editable';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\nimport {prefetchStrings} from 'core/prefetch';\nimport {getString} 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 {createScheduleModal} from 'core_reportbuilder/local/repository/modals';\nimport {deleteSchedule, sendSchedule, toggleSchedule} from 'core_reportbuilder/local/repository/schedules';\n\nlet initialized = false;\n\n/**\n * Initialise schedules tab\n *\n * @param {Number} reportId\n */\nexport const init = reportId => {\n prefetchStrings('core_reportbuilder', [\n 'deleteschedule',\n 'deletescheduleconfirm',\n 'disableschedule',\n 'editscheduledetails',\n 'enableschedule',\n 'newschedule',\n 'schedulecreated',\n 'scheduledeleted',\n 'schedulesent',\n 'scheduleupdated',\n 'sendschedule',\n 'sendscheduleconfirm',\n ]);\n\n prefetchStrings('core', [\n 'confirm',\n 'delete',\n ]);\n\n if (initialized) {\n // We already added the event listeners (can be called multiple times by mustache template).\n return;\n }\n\n document.addEventListener('click', event => {\n\n // Create schedule.\n const scheduleCreate = event.target.closest(reportSelectors.actions.scheduleCreate);\n if (scheduleCreate) {\n event.preventDefault();\n\n const scheduleModal = createScheduleModal(event.target, getString('newschedule', 'core_reportbuilder'), reportId);\n scheduleModal.addEventListener(scheduleModal.events.FORM_SUBMITTED, () => {\n getString('schedulecreated', 'core_reportbuilder')\n .then(addToast)\n .then(() => {\n const reportElement = document.querySelector(reportSelectors.regions.report);\n dispatchEvent(reportEvents.tableReload, {}, reportElement);\n return;\n })\n .catch(Notification.exception);\n });\n\n scheduleModal.show();\n }\n\n // Toggle schedule.\n const scheduleToggle = event.target.closest(reportSelectors.actions.scheduleToggle);\n if (scheduleToggle) {\n const pendingPromise = new Pending('core_reportbuilder/schedules:toggle');\n const scheduleStateToggle = +!Number(scheduleToggle.dataset.state);\n\n toggleSchedule(reportId, scheduleToggle.dataset.id, scheduleStateToggle)\n .then(() => {\n const tableRow = scheduleToggle.closest('tr');\n tableRow.classList.toggle('text-muted');\n\n scheduleToggle.dataset.state = scheduleStateToggle;\n\n const stringKey = scheduleStateToggle ? 'disableschedule' : 'enableschedule';\n return getString(stringKey, 'core_reportbuilder');\n })\n .then(toggleLabel => {\n const labelContainer = scheduleToggle.parentElement.querySelector(`label[for=\"${scheduleToggle.id}\"] > span`);\n labelContainer.innerHTML = toggleLabel;\n return pendingPromise.resolve();\n })\n .catch(Notification.exception);\n }\n\n // Edit schedule.\n const scheduleEdit = event.target.closest(reportSelectors.actions.scheduleEdit);\n if (scheduleEdit) {\n event.preventDefault();\n\n // Use triggerElement to return focus to the action menu toggle.\n const triggerElement = scheduleEdit.closest('.dropdown').querySelector('.dropdown-toggle');\n const scheduleModal = createScheduleModal(triggerElement, getString('editscheduledetails', 'core_reportbuilder'),\n reportId, scheduleEdit.dataset.scheduleId);\n scheduleModal.addEventListener(scheduleModal.events.FORM_SUBMITTED, () => {\n getString('scheduleupdated', 'core_reportbuilder')\n .then(addToast)\n .then(() => {\n const reportElement = scheduleEdit.closest(reportSelectors.regions.report);\n dispatchEvent(reportEvents.tableReload, {}, reportElement);\n return;\n })\n .catch(Notification.exception);\n });\n\n scheduleModal.show();\n }\n\n // Send schedule.\n const scheduleSend = event.target.closest(reportSelectors.actions.scheduleSend);\n if (scheduleSend) {\n event.preventDefault();\n\n // Use triggerElement to return focus to the action menu toggle.\n const triggerElement = scheduleSend.closest('.dropdown').querySelector('.dropdown-toggle');\n Notification.saveCancelPromise(\n getString('sendschedule', 'core_reportbuilder'),\n getString('sendscheduleconfirm', 'core_reportbuilder', scheduleSend.dataset.scheduleName),\n getString('confirm', 'core'),\n {triggerElement}\n ).then(() => {\n const pendingPromise = new Pending('core_reportbuilder/schedules:send');\n\n return sendSchedule(reportId, scheduleSend.dataset.scheduleId)\n .then(addToast(getString('schedulesent', 'core_reportbuilder')))\n .then(() => pendingPromise.resolve())\n .catch(Notification.exception);\n }).catch(() => {\n return;\n });\n }\n\n // Delete schedule.\n const scheduleDelete = event.target.closest(reportSelectors.actions.scheduleDelete);\n if (scheduleDelete) {\n event.preventDefault();\n\n // Use triggerElement to return focus to the action menu toggle.\n const triggerElement = scheduleDelete.closest('.dropdown').querySelector('.dropdown-toggle');\n Notification.saveCancelPromise(\n getString('deleteschedule', 'core_reportbuilder'),\n getString('deletescheduleconfirm', 'core_reportbuilder', scheduleDelete.dataset.scheduleName),\n getString('delete', 'core'),\n {triggerElement}\n ).then(() => {\n const pendingPromise = new Pending('core_reportbuilder/schedules:delete');\n\n return deleteSchedule(reportId, scheduleDelete.dataset.scheduleId)\n .then(addToast(getString('scheduledeleted', 'core_reportbuilder')))\n .then(() => {\n const reportElement = scheduleDelete.closest(reportSelectors.regions.report);\n dispatchEvent(reportEvents.tableReload, {preservePagination: true}, reportElement);\n return pendingPromise.resolve();\n })\n .catch(Notification.exception);\n }).catch(() => {\n return;\n });\n }\n });\n\n initialized = true;\n};\n"],"names":["initialized","reportId","document","addEventListener","event","target","closest","reportSelectors","actions","scheduleCreate","preventDefault","scheduleModal","events","FORM_SUBMITTED","then","addToast","reportElement","querySelector","regions","report","reportEvents","tableReload","catch","Notification","exception","show","scheduleToggle","pendingPromise","Pending","scheduleStateToggle","Number","dataset","state","id","classList","toggle","stringKey","toggleLabel","parentElement","innerHTML","resolve","scheduleEdit","triggerElement","scheduleId","scheduleSend","saveCancelPromise","scheduleName","scheduleDelete","preservePagination"],"mappings":"0wDAqCIA,aAAc,gBAOEC,yCACA,qBAAsB,CAClC,iBACA,wBACA,kBACA,sBACA,iBACA,cACA,kBACA,kBACA,eACA,kBACA,eACA,sDAGY,OAAQ,CACpB,UACA,WAGAD,cAKJE,SAASC,iBAAiB,SAASC,WAGRA,MAAMC,OAAOC,QAAQC,gBAAgBC,QAAQC,gBAChD,CAChBL,MAAMM,uBAEAC,eAAgB,+BAAoBP,MAAMC,QAAQ,kBAAU,cAAe,sBAAuBJ,UACxGU,cAAcR,iBAAiBQ,cAAcC,OAAOC,gBAAgB,wBACtD,kBAAmB,sBACxBC,KAAKC,YACLD,MAAK,WACIE,cAAgBd,SAASe,cAAcV,gBAAgBW,QAAQC,4CACvDC,aAAaC,YAAa,GAAIL,kBAG/CM,MAAMC,sBAAaC,cAG5Bb,cAAcc,aAIZC,eAAiBtB,MAAMC,OAAOC,QAAQC,gBAAgBC,QAAQkB,mBAChEA,eAAgB,OACVC,eAAiB,IAAIC,iBAAQ,uCAC7BC,sBAAwBC,OAAOJ,eAAeK,QAAQC,qCAE7C/B,SAAUyB,eAAeK,QAAQE,GAAIJ,qBAC/Cf,MAAK,KACeY,eAAepB,QAAQ,MAC/B4B,UAAUC,OAAO,cAE1BT,eAAeK,QAAQC,MAAQH,0BAEzBO,UAAYP,oBAAsB,kBAAoB,wBACrD,kBAAUO,UAAW,yBAE/BtB,MAAKuB,cACqBX,eAAeY,cAAcrB,mCAA4BS,eAAeO,iBAChFM,UAAYF,YACpBV,eAAea,aAEzBlB,MAAMC,sBAAaC,iBAItBiB,aAAerC,MAAMC,OAAOC,QAAQC,gBAAgBC,QAAQiC,iBAC9DA,aAAc,CACdrC,MAAMM,uBAGAgC,eAAiBD,aAAanC,QAAQ,aAAaW,cAAc,oBACjEN,eAAgB,+BAAoB+B,gBAAgB,kBAAU,sBAAuB,sBACvFzC,SAAUwC,aAAaV,QAAQY,YACnChC,cAAcR,iBAAiBQ,cAAcC,OAAOC,gBAAgB,wBACtD,kBAAmB,sBACxBC,KAAKC,YACLD,MAAK,WACIE,cAAgByB,aAAanC,QAAQC,gBAAgBW,QAAQC,4CACrDC,aAAaC,YAAa,GAAIL,kBAG/CM,MAAMC,sBAAaC,cAG5Bb,cAAcc,aAIZmB,aAAexC,MAAMC,OAAOC,QAAQC,gBAAgBC,QAAQoC,iBAC9DA,aAAc,CACdxC,MAAMM,uBAGAgC,eAAiBE,aAAatC,QAAQ,aAAaW,cAAc,0CAC1D4B,mBACT,kBAAU,eAAgB,uBAC1B,kBAAU,sBAAuB,qBAAsBD,aAAab,QAAQe,eAC5E,kBAAU,UAAW,QACrB,CAACJ,eAAAA,iBACH5B,MAAK,WACGa,eAAiB,IAAIC,iBAAQ,4CAE5B,2BAAa3B,SAAU2C,aAAab,QAAQY,YAC9C7B,MAAK,eAAS,kBAAU,eAAgB,wBACxCA,MAAK,IAAMa,eAAea,YAC1BlB,MAAMC,sBAAaC,cACzBF,OAAM,eAMPyB,eAAiB3C,MAAMC,OAAOC,QAAQC,gBAAgBC,QAAQuC,mBAChEA,eAAgB,CAChB3C,MAAMM,uBAGAgC,eAAiBK,eAAezC,QAAQ,aAAaW,cAAc,0CAC5D4B,mBACT,kBAAU,iBAAkB,uBAC5B,kBAAU,wBAAyB,qBAAsBE,eAAehB,QAAQe,eAChF,kBAAU,SAAU,QACpB,CAACJ,eAAAA,iBACH5B,MAAK,WACGa,eAAiB,IAAIC,iBAAQ,8CAE5B,6BAAe3B,SAAU8C,eAAehB,QAAQY,YAClD7B,MAAK,eAAS,kBAAU,kBAAmB,wBAC3CA,MAAK,WACIE,cAAgB+B,eAAezC,QAAQC,gBAAgBW,QAAQC,kDACvDC,aAAaC,YAAa,CAAC2B,oBAAoB,GAAOhC,eAC7DW,eAAea,aAEzBlB,MAAMC,sBAAaC,cACzBF,OAAM,aAMjBtB,aAAc"} \ No newline at end of file +{"version":3,"file":"schedules.min.js","sources":["../src/schedules.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Report builder audiences\n *\n * @module core_reportbuilder/schedules\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 {dispatchEvent} from 'core/event_dispatcher';\nimport 'core/inplace_editable';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\nimport {prefetchStrings} from 'core/prefetch';\nimport {getString} 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 {createScheduleModal} from 'core_reportbuilder/local/repository/modals';\nimport {deleteSchedule, sendSchedule, toggleSchedule} from 'core_reportbuilder/local/repository/schedules';\n\nlet initialized = false;\n\n/**\n * Initialise schedules tab\n *\n * @param {Number} reportId\n */\nexport const init = reportId => {\n prefetchStrings('core_reportbuilder', [\n 'deleteschedule',\n 'deletescheduleconfirm',\n 'disableschedule',\n 'editscheduledetails',\n 'enableschedule',\n 'newschedule',\n 'schedulecreated',\n 'scheduledeleted',\n 'schedulesent',\n 'scheduleupdated',\n 'sendschedule',\n 'sendscheduleconfirm',\n ]);\n\n prefetchStrings('core', [\n 'confirm',\n 'delete',\n ]);\n\n if (initialized) {\n // We already added the event listeners (can be called multiple times by mustache template).\n return;\n }\n\n document.addEventListener('click', event => {\n\n // Create schedule.\n const scheduleCreate = event.target.closest(reportSelectors.actions.scheduleCreate);\n if (scheduleCreate) {\n event.preventDefault();\n\n const {scheduleClass} = scheduleCreate.dataset;\n\n const scheduleModal = createScheduleModal(event.target, getString('newschedule', 'core_reportbuilder'), reportId, 0,\n scheduleClass);\n scheduleModal.addEventListener(scheduleModal.events.FORM_SUBMITTED, () => {\n getString('schedulecreated', 'core_reportbuilder')\n .then(addToast)\n .then(() => {\n const reportElement = document.querySelector(reportSelectors.regions.report);\n dispatchEvent(reportEvents.tableReload, {}, reportElement);\n return;\n })\n .catch(Notification.exception);\n });\n\n scheduleModal.show();\n }\n\n // Toggle schedule.\n const scheduleToggle = event.target.closest(reportSelectors.actions.scheduleToggle);\n if (scheduleToggle) {\n const pendingPromise = new Pending('core_reportbuilder/schedules:toggle');\n const scheduleStateToggle = +!Number(scheduleToggle.dataset.state);\n\n toggleSchedule(reportId, scheduleToggle.dataset.id, scheduleStateToggle)\n .then(() => {\n const tableRow = scheduleToggle.closest('tr');\n tableRow.classList.toggle('text-muted');\n\n scheduleToggle.dataset.state = scheduleStateToggle;\n\n const stringKey = scheduleStateToggle ? 'disableschedule' : 'enableschedule';\n return getString(stringKey, 'core_reportbuilder');\n })\n .then(toggleLabel => {\n const labelContainer = scheduleToggle.parentElement.querySelector(`label[for=\"${scheduleToggle.id}\"] > span`);\n labelContainer.innerHTML = toggleLabel;\n return pendingPromise.resolve();\n })\n .catch(Notification.exception);\n }\n\n // Edit schedule.\n const scheduleEdit = event.target.closest(reportSelectors.actions.scheduleEdit);\n if (scheduleEdit) {\n event.preventDefault();\n\n // Use triggerElement to return focus to the action menu toggle.\n const triggerElement = scheduleEdit.closest('.dropdown').querySelector('.dropdown-toggle');\n const scheduleModal = createScheduleModal(triggerElement, getString('editscheduledetails', 'core_reportbuilder'),\n reportId, scheduleEdit.dataset.scheduleId);\n scheduleModal.addEventListener(scheduleModal.events.FORM_SUBMITTED, () => {\n getString('scheduleupdated', 'core_reportbuilder')\n .then(addToast)\n .then(() => {\n const reportElement = scheduleEdit.closest(reportSelectors.regions.report);\n dispatchEvent(reportEvents.tableReload, {}, reportElement);\n return;\n })\n .catch(Notification.exception);\n });\n\n scheduleModal.show();\n }\n\n // Send schedule.\n const scheduleSend = event.target.closest(reportSelectors.actions.scheduleSend);\n if (scheduleSend) {\n event.preventDefault();\n\n // Use triggerElement to return focus to the action menu toggle.\n const triggerElement = scheduleSend.closest('.dropdown').querySelector('.dropdown-toggle');\n Notification.saveCancelPromise(\n getString('sendschedule', 'core_reportbuilder'),\n getString('sendscheduleconfirm', 'core_reportbuilder', scheduleSend.dataset.scheduleName),\n getString('confirm', 'core'),\n {triggerElement}\n ).then(() => {\n const pendingPromise = new Pending('core_reportbuilder/schedules:send');\n\n return sendSchedule(reportId, scheduleSend.dataset.scheduleId)\n .then(addToast(getString('schedulesent', 'core_reportbuilder')))\n .then(() => pendingPromise.resolve())\n .catch(Notification.exception);\n }).catch(() => {\n return;\n });\n }\n\n // Delete schedule.\n const scheduleDelete = event.target.closest(reportSelectors.actions.scheduleDelete);\n if (scheduleDelete) {\n event.preventDefault();\n\n // Use triggerElement to return focus to the action menu toggle.\n const triggerElement = scheduleDelete.closest('.dropdown').querySelector('.dropdown-toggle');\n Notification.saveCancelPromise(\n getString('deleteschedule', 'core_reportbuilder'),\n getString('deletescheduleconfirm', 'core_reportbuilder', scheduleDelete.dataset.scheduleName),\n getString('delete', 'core'),\n {triggerElement}\n ).then(() => {\n const pendingPromise = new Pending('core_reportbuilder/schedules:delete');\n\n return deleteSchedule(reportId, scheduleDelete.dataset.scheduleId)\n .then(addToast(getString('scheduledeleted', 'core_reportbuilder')))\n .then(() => {\n const reportElement = scheduleDelete.closest(reportSelectors.regions.report);\n dispatchEvent(reportEvents.tableReload, {preservePagination: true}, reportElement);\n return pendingPromise.resolve();\n })\n .catch(Notification.exception);\n }).catch(() => {\n return;\n });\n }\n });\n\n initialized = true;\n};\n"],"names":["initialized","reportId","document","addEventListener","event","scheduleCreate","target","closest","reportSelectors","actions","preventDefault","scheduleClass","dataset","scheduleModal","events","FORM_SUBMITTED","then","addToast","reportElement","querySelector","regions","report","reportEvents","tableReload","catch","Notification","exception","show","scheduleToggle","pendingPromise","Pending","scheduleStateToggle","Number","state","id","classList","toggle","stringKey","toggleLabel","parentElement","innerHTML","resolve","scheduleEdit","triggerElement","scheduleId","scheduleSend","saveCancelPromise","scheduleName","scheduleDelete","preservePagination"],"mappings":"0wDAqCIA,aAAc,gBAOEC,yCACA,qBAAsB,CAClC,iBACA,wBACA,kBACA,sBACA,iBACA,cACA,kBACA,kBACA,eACA,kBACA,eACA,sDAGY,OAAQ,CACpB,UACA,WAGAD,cAKJE,SAASC,iBAAiB,SAASC,cAGzBC,eAAiBD,MAAME,OAAOC,QAAQC,gBAAgBC,QAAQJ,mBAChEA,eAAgB,CAChBD,MAAMM,uBAEAC,cAACA,eAAiBN,eAAeO,QAEjCC,eAAgB,+BAAoBT,MAAME,QAAQ,kBAAU,cAAe,sBAAuBL,SAAU,EAC9GU,eACJE,cAAcV,iBAAiBU,cAAcC,OAAOC,gBAAgB,wBACtD,kBAAmB,sBACxBC,KAAKC,YACLD,MAAK,WACIE,cAAgBhB,SAASiB,cAAcX,gBAAgBY,QAAQC,4CACvDC,aAAaC,YAAa,GAAIL,kBAG/CM,MAAMC,sBAAaC,cAG5Bb,cAAcc,aAIZC,eAAiBxB,MAAME,OAAOC,QAAQC,gBAAgBC,QAAQmB,mBAChEA,eAAgB,OACVC,eAAiB,IAAIC,iBAAQ,uCAC7BC,sBAAwBC,OAAOJ,eAAehB,QAAQqB,qCAE7ChC,SAAU2B,eAAehB,QAAQsB,GAAIH,qBAC/Cf,MAAK,KACeY,eAAerB,QAAQ,MAC/B4B,UAAUC,OAAO,cAE1BR,eAAehB,QAAQqB,MAAQF,0BAEzBM,UAAYN,oBAAsB,kBAAoB,wBACrD,kBAAUM,UAAW,yBAE/BrB,MAAKsB,cACqBV,eAAeW,cAAcpB,mCAA4BS,eAAeM,iBAChFM,UAAYF,YACpBT,eAAeY,aAEzBjB,MAAMC,sBAAaC,iBAItBgB,aAAetC,MAAME,OAAOC,QAAQC,gBAAgBC,QAAQiC,iBAC9DA,aAAc,CACdtC,MAAMM,uBAGAiC,eAAiBD,aAAanC,QAAQ,aAAaY,cAAc,oBACjEN,eAAgB,+BAAoB8B,gBAAgB,kBAAU,sBAAuB,sBACvF1C,SAAUyC,aAAa9B,QAAQgC,YACnC/B,cAAcV,iBAAiBU,cAAcC,OAAOC,gBAAgB,wBACtD,kBAAmB,sBACxBC,KAAKC,YACLD,MAAK,WACIE,cAAgBwB,aAAanC,QAAQC,gBAAgBY,QAAQC,4CACrDC,aAAaC,YAAa,GAAIL,kBAG/CM,MAAMC,sBAAaC,cAG5Bb,cAAcc,aAIZkB,aAAezC,MAAME,OAAOC,QAAQC,gBAAgBC,QAAQoC,iBAC9DA,aAAc,CACdzC,MAAMM,uBAGAiC,eAAiBE,aAAatC,QAAQ,aAAaY,cAAc,0CAC1D2B,mBACT,kBAAU,eAAgB,uBAC1B,kBAAU,sBAAuB,qBAAsBD,aAAajC,QAAQmC,eAC5E,kBAAU,UAAW,QACrB,CAACJ,eAAAA,iBACH3B,MAAK,WACGa,eAAiB,IAAIC,iBAAQ,4CAE5B,2BAAa7B,SAAU4C,aAAajC,QAAQgC,YAC9C5B,MAAK,eAAS,kBAAU,eAAgB,wBACxCA,MAAK,IAAMa,eAAeY,YAC1BjB,MAAMC,sBAAaC,cACzBF,OAAM,eAMPwB,eAAiB5C,MAAME,OAAOC,QAAQC,gBAAgBC,QAAQuC,mBAChEA,eAAgB,CAChB5C,MAAMM,uBAGAiC,eAAiBK,eAAezC,QAAQ,aAAaY,cAAc,0CAC5D2B,mBACT,kBAAU,iBAAkB,uBAC5B,kBAAU,wBAAyB,qBAAsBE,eAAepC,QAAQmC,eAChF,kBAAU,SAAU,QACpB,CAACJ,eAAAA,iBACH3B,MAAK,WACGa,eAAiB,IAAIC,iBAAQ,8CAE5B,6BAAe7B,SAAU+C,eAAepC,QAAQgC,YAClD5B,MAAK,eAAS,kBAAU,kBAAmB,wBAC3CA,MAAK,WACIE,cAAgB8B,eAAezC,QAAQC,gBAAgBY,QAAQC,kDACvDC,aAAaC,YAAa,CAAC0B,oBAAoB,GAAO/B,eAC7DW,eAAeY,aAEzBjB,MAAMC,sBAAaC,cACzBF,OAAM,aAMjBxB,aAAc"} \ No newline at end of file diff --git a/public/reportbuilder/amd/src/local/repository/modals.js b/public/reportbuilder/amd/src/local/repository/modals.js index cc8268d76ed..47ce164bf6a 100644 --- a/public/reportbuilder/amd/src/local/repository/modals.js +++ b/public/reportbuilder/amd/src/local/repository/modals.js @@ -82,11 +82,13 @@ export const createDuplicateReportModal = (triggerElement, modalTitle, reportId, * @param {Promise} modalTitle * @param {Number} reportId * @param {Number} scheduleId + * @param {String} scheduleClass * @return {ModalForm} */ -export const createScheduleModal = (triggerElement, modalTitle, reportId, scheduleId = 0) => { +export const createScheduleModal = (triggerElement, modalTitle, reportId, scheduleId = 0, scheduleClass = '') => { return createModalForm(triggerElement, modalTitle, 'core_reportbuilder\\form\\schedule', { reportid: reportId, id: scheduleId, + classname: scheduleClass, }); }; diff --git a/public/reportbuilder/amd/src/schedules.js b/public/reportbuilder/amd/src/schedules.js index 2ff77ecc811..29ce12c3237 100644 --- a/public/reportbuilder/amd/src/schedules.js +++ b/public/reportbuilder/amd/src/schedules.js @@ -75,7 +75,10 @@ export const init = reportId => { if (scheduleCreate) { event.preventDefault(); - const scheduleModal = createScheduleModal(event.target, getString('newschedule', 'core_reportbuilder'), reportId); + const {scheduleClass} = scheduleCreate.dataset; + + const scheduleModal = createScheduleModal(event.target, getString('newschedule', 'core_reportbuilder'), reportId, 0, + scheduleClass); scheduleModal.addEventListener(scheduleModal.events.FORM_SUBMITTED, () => { getString('schedulecreated', 'core_reportbuilder') .then(addToast) diff --git a/public/reportbuilder/classes/form/schedule.php b/public/reportbuilder/classes/form/schedule.php index a9be825e83f..09445f9efd1 100644 --- a/public/reportbuilder/classes/form/schedule.php +++ b/public/reportbuilder/classes/form/schedule.php @@ -26,10 +26,10 @@ use core\output\notification; use core_form\dynamic_form; use core_reportbuilder\manager; use core_reportbuilder\permission; -use core_reportbuilder\local\helpers\audience; -use core_reportbuilder\local\helpers\schedule as helper; +use core_reportbuilder\local\helpers\{audience, schedule as helper}; use core_reportbuilder\local\models\schedule as model; -use core_reportbuilder\local\report\base; +use core_reportbuilder\local\schedules\base; +use core_reportbuilder\local\report\base as report_base; /** * Schedule form @@ -41,11 +41,30 @@ use core_reportbuilder\local\report\base; class schedule extends dynamic_form { /** - * Return instance of the system report using the filter form + * Return schedule instance * * @return base */ - private function get_report(): base { + private function get_schedule(): base { + $reportid = $this->optional_param('reportid', 0, PARAM_INT); + $scheduleid = $this->optional_param('id', 0, PARAM_INT); + + if ($scheduleid > 0) { + $schedule = model::get_record(['id' => $scheduleid, 'reportid' => $reportid], MUST_EXIST); + return base::from_persistent($schedule); + } else { + /** @var base $scheduleclass */ + $scheduleclass = $this->optional_param('classname', '', PARAM_RAW); + return $scheduleclass::instance(); + } + } + + /** + * Return instance of the system report using the filter form + * + * @return report_base + */ + private function get_report(): report_base { $reportid = $this->optional_param('reportid', 0, PARAM_INT); return manager::get_report_from_id($reportid); } @@ -83,6 +102,9 @@ class schedule extends dynamic_form { $mform->addElement('hidden', 'id'); $mform->setType('id', PARAM_INT); + $mform->addElement('hidden', 'classname'); + $mform->setType('id', PARAM_RAW); + // General fields. $mform->addElement('header', 'headergeneral', get_string('general')); @@ -121,63 +143,47 @@ class schedule extends dynamic_form { } // Audience fields. - $mform->addElement('header', 'headeraudience', get_string('audience', 'core_reportbuilder')); - $mform->setExpanded('headeraudience', true); + $schedule = $this->get_schedule(); + if ($schedule->requires_audience()) { + $mform->addElement('header', 'headeraudience', get_string('audience', 'core_reportbuilder')); + $mform->setExpanded('headeraudience', true); - $audiences = audience::get_base_records($this->optional_param('reportid', 0, PARAM_INT)); - if (empty($audiences)) { - $notification = new notification(get_string('noaudiences', 'core_reportbuilder'), notification::NOTIFY_INFO, false); - $mform->addElement('static', 'noaudiences', '', $OUTPUT->render($notification)); - } - - $audiencecheckboxes = []; - foreach ($audiences as $audience) { - $persistent = $audience->get_persistent(); - - // Check for a custom name, otherwise fall back to default. - if ('' === $audiencelabel = $persistent->get_formatted_heading($context)) { - $audiencelabel = get_string('audiencelabel', 'core_reportbuilder', (object) [ - 'name' => $audience->get_name(), - 'description' => $audience->get_description(), - ]); + $audiences = audience::get_base_records($this->optional_param('reportid', 0, PARAM_INT)); + if (empty($audiences)) { + $notification = new notification(get_string('noaudiences', 'core_reportbuilder'), notification::NOTIFY_INFO, false); + $mform->addElement('static', 'noaudiences', '', $OUTPUT->render($notification)); } - $audiencecheckboxes[] = $mform->createElement('checkbox', $persistent->get('id'), $audiencelabel); + $audiencecheckboxes = []; + foreach ($audiences as $audience) { + $persistent = $audience->get_persistent(); + + // Check for a custom name, otherwise fall back to default. + if ('' === $audiencelabel = $persistent->get_formatted_heading($context)) { + $audiencelabel = get_string('audiencelabel', 'core_reportbuilder', (object) [ + 'name' => $audience->get_name(), + 'description' => $audience->get_description(), + ]); + } + + $audiencecheckboxes[] = $mform->createElement('checkbox', $persistent->get('id'), $audiencelabel); + } + + $mform->addElement('group', 'audiences', '', $audiencecheckboxes, html_writer::div('', 'w-100 mb-2')); } - $mform->addElement('group', 'audiences', '', $audiencecheckboxes, html_writer::div('', 'w-100 mb-2')); - - // Message fields. - $mform->addElement('header', 'headermessage', get_string('messagecontent', 'core_reportbuilder')); - - $mform->addElement('text', 'subject', get_string('messagesubject', 'core_reportbuilder')); - $mform->setType('subject', PARAM_TEXT); - $mform->addRule('subject', null, 'required', null, 'client'); - $mform->addRule('subject', get_string('maximumchars', '', 255), 'maxlength', 255); - - $mform->addElement('editor', 'message', get_string('messagebody', 'core_reportbuilder'), null, ['autosave' => false]); - $mform->setType('message', PARAM_RAW); - $mform->addRule('message', null, 'required', null, 'client'); - - // Advanced. - $mform->addElement('header', 'headeradvanced', get_string('advanced')); - - $mform->addElement('select', 'reportempty', get_string('scheduleempty', 'core_reportbuilder'), - helper::get_report_empty_options()); - $mform->setType('reportempty', PARAM_INT); + // Load schedule type form definition. + $schedule->definition($mform); } /** * Load form data if we are editing an existing schedule */ public function set_data_for_dynamic_submission(): void { - $reportid = $this->optional_param('reportid', 0, PARAM_INT); - $scheduleid = $this->optional_param('id', 0, PARAM_INT); + $schedule = $this->get_schedule(); - if ($scheduleid > 0) { - $schedule = model::get_record(['id' => $scheduleid, 'reportid' => $reportid]); - - $data = (array) $schedule->to_record(); + if ($schedule->get_persistent()->get('id') > 0) { + $data = (array) $schedule->get_persistent()->to_record(); // Pre-process some of the form fields. if (!in_array($data['userviewas'], [model::REPORT_VIEWAS_CREATOR, model::REPORT_VIEWAS_RECIPIENT])) { @@ -185,17 +191,18 @@ class schedule extends dynamic_form { $data['userviewas'] = model::REPORT_VIEWAS_USER; } - $audiences = json_decode($data['audiences']); - $data['audiences'] = array_fill_keys($audiences, 1); + if ($schedule->requires_audience()) { + $audiences = (array) json_decode((string) $data['audiences']); + $data['audiences'] = array_fill_keys($audiences, 1); + } - $data['message'] = [ - 'text' => $data['message'], - 'format' => $data['messageformat'], - ]; + // Load schedule type form definition data. + $data['configdata'] = $schedule->get_configdata(); $this->set_data($data); } else { - $this->set_data(['reportid' => $reportid]); + $reportid = $this->optional_param('reportid', 0, PARAM_INT); + $this->set_data(['reportid' => $reportid, 'classname' => $schedule::class]); } } @@ -220,11 +227,13 @@ class schedule extends dynamic_form { $errors['user'] = get_string('required'); } - if (empty($data['audiences'])) { + // Load schedule type form validation. + $schedule = $this->get_schedule(); + if ($schedule->requires_audience() && empty($data['audiences'])) { $errors['audiences'] = get_string('required'); } - return $errors; + return array_merge($errors, $schedule->validate($data, $files)); } /** @@ -238,13 +247,21 @@ class schedule extends dynamic_form { $data->userviewas = (int) $data->user; } - $data->audiences = json_encode(array_keys($data->audiences)); - ['text' => $data->message, 'format' => $data->messageformat] = $data->message; + $schedule = $this->get_schedule(); - if ($data->id) { + if ($schedule->requires_audience()) { + $data->audiences = json_encode(array_keys($data->audiences)); + } else { + $data->audiences = null; + } + + $data->configdata = json_encode((array) $data->configdata); + + if ($schedule->get_persistent()->get('id') > 0) { helper::update_schedule($data); } else { - helper::create_schedule($data); + unset($data->id); + $schedule::create($data); } } diff --git a/public/reportbuilder/classes/local/helpers/audience.php b/public/reportbuilder/classes/local/helpers/audience.php index 6067f6d5cb1..795451f3014 100644 --- a/public/reportbuilder/classes/local/helpers/audience.php +++ b/public/reportbuilder/classes/local/helpers/audience.php @@ -264,7 +264,7 @@ class audience { public static function get_audiences_for_report_schedules(int $reportid): array { global $DB; - $audiences = $DB->get_fieldset_select(schedule::TABLE, 'audiences', 'reportid = ?', [$reportid]); + $audiences = $DB->get_fieldset_select(schedule::TABLE, 'audiences', 'reportid = ? AND audiences IS NOT NULL', [$reportid]); // Reduce JSON encoded audience data of each schedule to an array of audience IDs. $audienceids = array_reduce($audiences, static function(array $carry, string $audience): array { diff --git a/public/reportbuilder/classes/local/helpers/report.php b/public/reportbuilder/classes/local/helpers/report.php index cf9df877a1a..85f29d12e46 100644 --- a/public/reportbuilder/classes/local/helpers/report.php +++ b/public/reportbuilder/classes/local/helpers/report.php @@ -171,7 +171,7 @@ class report { // Map new audience ids with the old ones. $audiences = array_map( fn($audienceid) => $audiencemap[$audienceid] ?? 0, - (array) json_decode($schedulerecord->audiences), + (array) json_decode((string) $schedulerecord->audiences), ); (new schedule(0, $schedulerecord)) diff --git a/public/reportbuilder/classes/local/helpers/schedule.php b/public/reportbuilder/classes/local/helpers/schedule.php index 121ea728971..481381bb115 100644 --- a/public/reportbuilder/classes/local/helpers/schedule.php +++ b/public/reportbuilder/classes/local/helpers/schedule.php @@ -19,7 +19,7 @@ declare(strict_types=1); namespace core_reportbuilder\local\helpers; use context_user; -use core\{clock, di}; +use core\{component, clock, di}; use core\exception\{coding_exception, invalid_parameter_exception}; use core_user; use stdClass; @@ -29,6 +29,7 @@ use core\message\message; use core\plugininfo\dataformat; use core_reportbuilder\local\models\audience as audience_model; use core_reportbuilder\local\models\schedule as model; +use core_reportbuilder\local\schedules\base; use core_reportbuilder\table\custom_report_table_view; /** @@ -39,6 +40,26 @@ use core_reportbuilder\table\custom_report_table_view; * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class schedule { + /** + * Determine whether given class is a valid schedule type + * + * @param string $scheduleclass + * @return bool + */ + public static function valid(string $scheduleclass): bool { + return class_exists($scheduleclass) && is_subclass_of($scheduleclass, base::class); + } + + /** + * Return list of all available/valid schedule types + * + * @return base[] + */ + public static function get_schedules(): array { + $classes = component::get_component_classes_in_namespace(null, 'reportbuilder\\schedule'); + + return array_filter(array_keys($classes), [static::class, 'valid']); + } /** * Create report schedule, calculate when it should be next sent @@ -46,18 +67,18 @@ class schedule { * @param stdClass $data * @param int|null $timenow Deprecated since Moodle 4.5 - please use {@see clock} dependency injection * @return model + * + * @deprecated since Moodle 5.1 - please do not use this function any more, {@see base::create} */ + #[\core\attribute\deprecated(base::class . '::create', since: '5.1', mdl: 'MDL-86066')] public static function create_schedule(stdClass $data, ?int $timenow = null): model { + \core\deprecation::emit_deprecation([self::class, __FUNCTION__]); + if ($timenow !== null) { debugging('Passing $timenow is deprecated, please use \core\clock dependency injection', DEBUG_DEVELOPER); } - $data->name = trim($data->name); - - $schedule = (new model(0, $data)); - $schedule->set('timenextsend', self::calculate_next_send_time($schedule)); - - return $schedule->create(); + return base::create($data)->get_persistent(); } /** @@ -113,7 +134,7 @@ class schedule { public static function get_schedule_report_users(model $schedule): array { global $DB; - $audienceids = (array) json_decode($schedule->get('audiences')); + $audienceids = (array) json_decode((string) $schedule->get('audiences')); // Retrieve all selected audience records for the schedule. [$audienceselect, $audienceparams] = $DB->get_in_or_equal($audienceids, SQL_PARAMS_NAMED, 'aid', true, null); @@ -317,8 +338,13 @@ class schedule { * @param stdClass $user * @param stored_file $attachment * @return bool + * + * @deprecated since Moodle 5.1 - please do not use this function any more */ + #[\core\attribute\deprecated(reason: 'It is no longer used', since: '5.1', mdl: 'MDL-86066')] public static function send_schedule_message(model $schedule, stdClass $user, stored_file $attachment): bool { + \core\deprecation::emit_deprecation([self::class, __FUNCTION__]); + $message = new message(); $message->component = 'moodle'; $message->name = 'reportbuilderschedule'; @@ -402,12 +428,17 @@ class schedule { * Return list of options for when report is empty * * @return string[] + * + * @deprecated since Moodle 5.1 - please do not use this function any more */ + #[\core\attribute\deprecated(reason: 'It is no longer used', since: '5.1', mdl: 'MDL-86066')] public static function get_report_empty_options(): array { + \core\deprecation::emit_deprecation([self::class, __FUNCTION__]); + return [ - model::REPORT_EMPTY_SEND_EMPTY => get_string('scheduleemptysendwithattachment', 'core_reportbuilder'), - model::REPORT_EMPTY_SEND_WITHOUT => get_string('scheduleemptysendwithoutattachment', 'core_reportbuilder'), - model::REPORT_EMPTY_DONT_SEND => get_string('scheduleemptydontsend', 'core_reportbuilder'), + 0 => get_string('scheduleemptysendwithattachment', 'core_reportbuilder'), + 1 => get_string('scheduleemptysendwithoutattachment', 'core_reportbuilder'), + 2 => get_string('scheduleemptydontsend', 'core_reportbuilder'), ]; } } diff --git a/public/reportbuilder/classes/local/models/schedule.php b/public/reportbuilder/classes/local/models/schedule.php index d468d5ec2f3..f28003e8deb 100644 --- a/public/reportbuilder/classes/local/models/schedule.php +++ b/public/reportbuilder/classes/local/models/schedule.php @@ -67,15 +67,6 @@ class schedule extends persistent { /** @var int Annual recurrence */ public const RECURRENCE_ANNUALLY = 5; - /** @var int Send schedule with empty report */ - public const REPORT_EMPTY_SEND_EMPTY = 0; - - /** @var int Send schedule without report */ - public const REPORT_EMPTY_SEND_WITHOUT = 1; - - /** @var int Don't send schedule if report is empty */ - public const REPORT_EMPTY_DONT_SEND = 2; - /** * Return the definition of the properties of this model. * @@ -95,27 +86,19 @@ class schedule extends persistent { ], 'audiences' => [ 'type' => PARAM_RAW, - 'default' => '[]', + 'null' => NULL_ALLOWED, + 'default' => null, + ], + 'classname' => [ + 'type' => PARAM_TEXT, + ], + 'configdata' => [ + 'type' => PARAM_RAW, + 'default' => '{}', ], 'format' => [ 'type' => PARAM_PLUGIN, ], - 'subject' => [ - 'type' => PARAM_TEXT, - ], - 'message' => [ - 'type' => PARAM_CLEANHTML, - ], - 'messageformat' => [ - 'type' => PARAM_INT, - 'default' => FORMAT_HTML, - 'choices' => [ - FORMAT_MOODLE, - FORMAT_HTML, - FORMAT_PLAIN, - FORMAT_MARKDOWN, - ], - ], 'userviewas' => [ 'type' => PARAM_INT, 'default' => self::REPORT_VIEWAS_CREATOR, @@ -136,15 +119,6 @@ class schedule extends persistent { self::RECURRENCE_ANNUALLY, ], ], - 'reportempty' => [ - 'type' => PARAM_INT, - 'default' => self::REPORT_EMPTY_SEND_EMPTY, - 'choices' => [ - self::REPORT_EMPTY_SEND_EMPTY, - self::REPORT_EMPTY_SEND_WITHOUT, - self::REPORT_EMPTY_DONT_SEND, - ], - ], 'timelastsent' => [ 'type' => PARAM_INT, 'default' => 0, diff --git a/public/reportbuilder/classes/local/schedules/base.php b/public/reportbuilder/classes/local/schedules/base.php new file mode 100644 index 00000000000..98c04175c45 --- /dev/null +++ b/public/reportbuilder/classes/local/schedules/base.php @@ -0,0 +1,176 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\local\schedules; + +use core_reportbuilder\local\helpers\schedule as helper; +use core_reportbuilder\local\models\schedule; +use MoodleQuickForm; +use progress_trace; +use stdClass; + +/** + * Schedule base class + * + * @package core_reportbuilder + * @copyright 2025 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class base { + /** + * Private constructor, please use {@see instance} or {@see from_persistent} methods instead + * + * @param schedule $schedule The persistent object associated with this schedule + */ + private function __construct( + /** @var schedule The persistent object associated with this schedule */ + private schedule $schedule, + ) { + // Nothing to see here. + } + + /** + * Load instance of schedule type + * + * @param int $id + * @param stdClass|null $record + * @return self|null + */ + final public static function instance(int $id = 0, ?stdClass $record = null): ?self { + $schedule = new schedule($id, $record); + return static::from_persistent($schedule); + } + + /** + * Load instance of schedule type from persistent + * + * @param schedule $schedule + * @return self|null + */ + final public static function from_persistent(schedule $schedule): ?self { + // Ensure schedule class is always populated. + if (!$classname = $schedule->get('classname')) { + $classname = get_called_class(); + $schedule->set('classname', $classname); + } + + if (!helper::valid($classname)) { + return null; + } + + return new $classname($schedule); + } + + /** + * Create instance of schedule type from record + * + * @param stdClass $record + * @return self + */ + final public static function create(stdClass $record): self { + $schedule = new schedule(0, $record); + + $schedule->set_many([ + 'name' => trim($schedule->get('name')), + 'timenextsend' => helper::calculate_next_send_time($schedule), + ]); + + $instance = self::from_persistent($schedule); + $instance->get_persistent()->create(); + + return $instance; + } + + /** + * Name of the schedule + * + * @return string + */ + abstract public function get_name(): string; + + /** + * Description of the schedule + * + * @return string + */ + abstract public function get_description(): string; + + /** + * If the current user is able to add this schedule type + * + * @return bool + */ + public function user_can_add(): bool { + return true; + } + + /** + * Whether the schedule requires audience configuration + * + * @return bool + */ + public function requires_audience(): bool { + return true; + } + + /** + * Schedule specific form definition elements + * + * @param MoodleQuickForm $mform + */ + abstract public function definition(MoodleQuickForm $mform): void; + + /** + * Validate schedule specific form data + * + * @param array $data + * @param array $files + * @return array + */ + public function validate(array $data, array $files): array { + return []; + } + + /** + * Execute the schedule. If {@see requires_audience}, then a list of audience user records will be passed as parameter + * + * Will be called via cron as part of the {@see \core_reportbuilder\task\send_schedule} task + * + * @param stdClass[] $users + * @param progress_trace $trace + */ + abstract public function execute(array $users, progress_trace $trace): void; + + /** + * Return schedule persistent + * + * @return schedule + */ + final public function get_persistent(): schedule { + return $this->schedule; + } + + /** + * Return decoded schedule config + * + * @return array + */ + final public function get_configdata(): array { + return json_decode($this->schedule->get('configdata'), true); + } +} diff --git a/public/reportbuilder/classes/local/systemreports/report_schedules.php b/public/reportbuilder/classes/local/systemreports/report_schedules.php index 58167a184d4..a4bca7d664a 100644 --- a/public/reportbuilder/classes/local/systemreports/report_schedules.php +++ b/public/reportbuilder/classes/local/systemreports/report_schedules.php @@ -30,6 +30,7 @@ use core_reportbuilder\local\filters\{boolean_select, date, text}; use core_reportbuilder\local\helpers\format; use core_reportbuilder\local\models\{report, schedule}; use core_reportbuilder\local\report\{action, column, filter}; +use core_reportbuilder\local\schedules\base; use core_reportbuilder\output\schedule_name_editable; /** @@ -60,7 +61,7 @@ class report_schedules extends system_report { $this->add_base_condition_simple('sc.reportid', $this->get_parameter('reportid', 0, PARAM_INT)); // Select fields required for actions, permission checks, and row class callbacks. - $this->add_base_fields('sc.id, sc.name, sc.enabled, rb.contextid'); + $this->add_base_fields('sc.id, sc.name, sc.enabled, sc.classname, rb.contextid'); // Join user entity for "User modified" column. $entityuser = new user(); @@ -310,31 +311,38 @@ class report_schedules extends system_report { */ protected function add_actions(): void { // Edit action. - $this->add_action(new action( + $this->add_action((new action( new moodle_url('#'), new pix_icon('t/edit', ''), ['data-action' => 'schedule-edit', 'data-schedule-id' => ':id'], false, new lang_string('editscheduledetails', 'core_reportbuilder') - )); + )) + ->add_callback(static function (stdClass $row): bool { + $instance = base::instance(0, $row); + return $instance !== null && $instance->user_can_add(); + })); // Send now action. $this->add_action((new action( new moodle_url('#'), - new pix_icon('t/email', ''), + new pix_icon('t/play', ''), ['data-action' => 'schedule-send', 'data-schedule-id' => ':id', 'data-schedule-name' => ':name'], false, new lang_string('sendschedule', 'core_reportbuilder') )) ->add_callback(function(stdClass $row): bool { + $instance = base::instance(0, $row); + if ($instance === null || !$instance->user_can_add()) { + return false; + } // Ensure data name attribute is properly formatted. - $row->name = (new schedule(0, $row))->get_formatted_name( + $row->name = $instance->get_persistent()->get_formatted_name( context::instance_by_id($row->contextid)); return true; - }) - ); + })); // Delete action. $this->add_action((new action( @@ -350,13 +358,16 @@ class report_schedules extends system_report { new lang_string('deleteschedule', 'core_reportbuilder') )) ->add_callback(function(stdClass $row): bool { + $instance = base::instance(0, $row); + if ($instance !== null && !$instance->user_can_add()) { + return false; + } // Ensure data name attribute is properly formatted. $row->name = (new schedule(0, $row))->get_formatted_name( context::instance_by_id($row->contextid)); return true; - }) - ); + })); } } diff --git a/public/reportbuilder/classes/output/dynamictabs/schedules.php b/public/reportbuilder/classes/output/dynamictabs/schedules.php index 6bb81407d74..17c02c3b342 100644 --- a/public/reportbuilder/classes/output/dynamictabs/schedules.php +++ b/public/reportbuilder/classes/output/dynamictabs/schedules.php @@ -19,13 +19,14 @@ declare(strict_types=1); namespace core_reportbuilder\output\dynamictabs; use context_system; -use renderer_base; +use core\output\{choicelist, renderer_base}; +use core\output\local\dropdown\{dialog, status}; use core\output\dynamic_tabs\base; use core_reportbuilder\permission; use core_reportbuilder\system_report_factory; +use core_reportbuilder\local\helpers\schedule; use core_reportbuilder\local\models\report; use core_reportbuilder\local\systemreports\report_schedules; -use core_reportbuilder\output\report_action; /** * Schedules dynamic tab @@ -45,12 +46,38 @@ class schedules extends base { public function export_for_template(renderer_base $output): array { $report = system_report_factory::create(report_schedules::class, context_system::instance(), '', '', 0, ['reportid' => $this->data['reportid']]); - $report->set_report_action(new report_action( + + // Schedule type menu. + $choicelist = new choicelist(); + $choicelist->set_allow_empty(true); + + // Include those schedule types the user can add. + foreach (schedule::get_schedules() as $schedule) { + $instance = $schedule::instance(); + if ($instance->user_can_add()) { + $choicelist->add_option( + $instance::class, + $instance->get_name(), + [ + 'description' => $instance->get_description(), + 'extras' => ['data-action' => 'schedule-create', 'data-schedule-class' => $instance::class], + ] + ); + } + } + + $dialog = new status( get_string('newschedule', 'core_reportbuilder'), - ['class' => 'btn btn-primary ms-auto', 'data-action' => 'schedule-create'], - )); + $choicelist, + [ + 'classes' => 'd-flex justify-content-end mb-2', + 'buttonclasses' => 'btn btn-primary dropdown-toggle', + ], + ); + $dialog->set_dialog_width(dialog::WIDTH['small']); return [ + 'menu' => $dialog->export_for_template($output), 'reportid' => $this->data['reportid'], 'report' => $report->output(), ]; diff --git a/public/reportbuilder/classes/privacy/provider.php b/public/reportbuilder/classes/privacy/provider.php index 87bc5d3284e..4eb92b35dc1 100644 --- a/public/reportbuilder/classes/privacy/provider.php +++ b/public/reportbuilder/classes/privacy/provider.php @@ -30,6 +30,7 @@ use core_privacy\local\request\userlist; use core_privacy\local\request\writer; use core_reportbuilder\manager; use core_reportbuilder\local\helpers\schedule as schedule_helper; +use core_reportbuilder\local\schedules\base; use core_reportbuilder\local\models\{audience, column, filter, report, schedule, user_filter}; /** @@ -95,13 +96,12 @@ class provider implements 'name' => 'privacy:metadata:schedule:name', 'enabled' => 'privacy:metadata:schedule:enabled', 'audiences' => 'privacy:metadata:schedule:audiences', + 'classname' => 'privacy:metadata:schedule:classname', + 'configdata' => 'privacy:metadata:schedule:configdata', 'format' => 'privacy:metadata:schedule:format', - 'subject' => 'privacy:metadata:schedule:subject', - 'message' => 'privacy:metadata:schedule:message', 'userviewas' => 'privacy:metadata:schedule:userviewas', 'timescheduled' => 'privacy:metadata:schedule:timescheduled', 'recurrence' => 'privacy:metadata:schedule:recurrence', - 'reportempty' => 'privacy:metadata:schedule:reportempty', 'usercreated' => 'privacy:metadata:schedule:usercreated', 'usermodified' => 'privacy:metadata:schedule:usermodified', 'timecreated' => 'privacy:metadata:schedule:timecreated', @@ -367,31 +367,38 @@ class provider implements $formatoptions = schedule_helper::get_format_options(); $recurrenceoptions = schedule_helper::get_recurrence_options(); $viewasoptions = schedule_helper::get_viewas_options(); - $reportemptyoptions = schedule_helper::get_report_empty_options(); - $scheduledata = array_map(static function(schedule $schedule) use ( - $context, $formatoptions, $recurrenceoptions, $viewasoptions, $reportemptyoptions): stdClass { + $scheduledata = array_map( + static function (schedule $schedule) use ( + $context, + $formatoptions, + $recurrenceoptions, + $viewasoptions, + ): stdClass { + // Show the schedule name, if it exists. + $scheduleinstance = base::from_persistent($schedule); - // The "User view as" property will be either creator, recipient or a specific userid. - $userviewas = $schedule->get('userviewas'); + // The "User view as" property will be either creator, recipient or a specific userid. + $userviewas = $schedule->get('userviewas'); - return (object) [ - 'name' => $schedule->get_formatted_name($context), - 'enabled' => transform::yesno($schedule->get('enabled')), - 'format' => $formatoptions[$schedule->get('format')], - 'timescheduled' => transform::datetime($schedule->get('timescheduled')), - 'recurrence' => $recurrenceoptions[$schedule->get('recurrence')], - 'userviewas' => $viewasoptions[$userviewas] ?? transform::user($userviewas), - 'audiences' => $schedule->get('audiences'), - 'subject' => $schedule->get('subject'), - 'message' => format_text($schedule->get('message'), $schedule->get('messageformat'), ['context' => $context]), - 'reportempty' => $reportemptyoptions[$schedule->get('reportempty')], - 'usercreated' => transform::user($schedule->get('usercreated')), - 'usermodified' => transform::user($schedule->get('usermodified')), - 'timecreated' => transform::datetime($schedule->get('timecreated')), - 'timemodified' => transform::datetime($schedule->get('timemodified')), - ]; - }, $schedules); + return (object) [ + 'name' => $schedule->get_formatted_name($context), + 'enabled' => transform::yesno($schedule->get('enabled')), + 'classname' => $scheduleinstance?->get_name() ?? $schedule->get('classname'), + 'configdata' => $schedule->get('configdata'), + 'format' => $formatoptions[$schedule->get('format')], + 'timescheduled' => transform::datetime($schedule->get('timescheduled')), + 'recurrence' => $recurrenceoptions[$schedule->get('recurrence')], + 'userviewas' => $viewasoptions[$userviewas] ?? transform::user($userviewas), + 'audiences' => $schedule->get('audiences'), + 'usercreated' => transform::user($schedule->get('usercreated')), + 'usermodified' => transform::user($schedule->get('usermodified')), + 'timecreated' => transform::datetime($schedule->get('timecreated')), + 'timemodified' => transform::datetime($schedule->get('timemodified')), + ]; + }, + $schedules, + ); writer::with_context($context)->export_related_data($subcontext, 'schedules', (object) ['data' => $scheduledata]); } diff --git a/public/reportbuilder/classes/reportbuilder/schedule/message.php b/public/reportbuilder/classes/reportbuilder/schedule/message.php new file mode 100644 index 00000000000..d3c0799cc83 --- /dev/null +++ b/public/reportbuilder/classes/reportbuilder/schedule/message.php @@ -0,0 +1,178 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\reportbuilder\schedule; + +use core\exception\moodle_exception; +use core\user; +use core_reportbuilder\local\helpers\{report, schedule as helper}; +use core_reportbuilder\local\models\schedule; +use core_reportbuilder\local\schedules\base; +use MoodleQuickForm; +use progress_trace; +use stdClass; +use stored_file; + +/** + * Message schedule class + * + * @package core_reportbuilder + * @copyright 2025 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class message extends base { + /** @var int Send schedule with empty report */ + public const REPORT_EMPTY_SEND_EMPTY = 0; + + /** @var int Send schedule without report */ + public const REPORT_EMPTY_SEND_WITHOUT = 1; + + /** @var int Don't send schedule if report is empty */ + public const REPORT_EMPTY_DONT_SEND = 2; + + #[\Override] + public function get_name(): string { + return get_string('scheduleemail', 'core_reportbuilder'); + } + + #[\Override] + public function get_description(): string { + return get_string('scheduleemaildescription', 'core_reportbuilder'); + } + + #[\Override] + public function definition(MoodleQuickForm $mform): void { + // Message fields. + $mform->addElement('header', 'headermessage', get_string('messagecontent', 'core_reportbuilder')); + + $mform->addElement('text', 'configdata[subject]', get_string('messagesubject', 'core_reportbuilder')); + $mform->setType('configdata[subject]', PARAM_TEXT); + $mform->addRule('configdata[subject]', null, 'required', null, 'client'); + $mform->addRule('configdata[subject]', get_string('maximumchars', '', 255), 'maxlength', 255); + + $mform->addElement('editor', 'configdata[message]', get_string('messagebody', 'core_reportbuilder'), null, [ + 'autosave' => false, + ]); + $mform->setType('configdata[message]', PARAM_RAW); + $mform->addRule('configdata[message]', null, 'required', null, 'client'); + + // Advanced. + $mform->addElement('header', 'headeradvanced', get_string('advanced')); + + $mform->addElement('select', 'configdata[reportempty]', get_string('scheduleempty', 'core_reportbuilder'), [ + static::REPORT_EMPTY_SEND_EMPTY => get_string('scheduleemptysendwithattachment', 'core_reportbuilder'), + static::REPORT_EMPTY_SEND_WITHOUT => get_string('scheduleemptysendwithoutattachment', 'core_reportbuilder'), + static::REPORT_EMPTY_DONT_SEND => get_string('scheduleemptydontsend', 'core_reportbuilder'), + ]); + } + + #[\Override] + public function execute(array $users, progress_trace $trace): void { + $scheduleattachment = null; + + if (count($users) > 0) { + $schedule = $this->get_persistent(); + + $scheduleuserviewas = $schedule->get('userviewas'); + $schedulereportempty = $this->get_configdata()['reportempty'] ?? static::REPORT_EMPTY_SEND_EMPTY; + + // Handle schedule configuration as to who the report should be viewed as. + if ($scheduleuserviewas === schedule::REPORT_VIEWAS_CREATOR) { + $scheduleattachment = helper::get_schedule_report_file($schedule); + } else if ($scheduleuserviewas !== schedule::REPORT_VIEWAS_RECIPIENT) { + // Get the user to view the schedule report as, ensure it's an active account. + try { + $scheduleviewas = user::get_user($scheduleuserviewas, '*', MUST_EXIST); + user::require_active_user($scheduleviewas); + } catch (moodle_exception $exception) { + $trace->output('Invalid schedule view as user: ' . $exception->getMessage()); + return; + } + + \core\cron::setup_user($scheduleviewas); + $scheduleattachment = helper::get_schedule_report_file($schedule); + } + + // Apply special handling if report is empty (default is to send it anyway). + if ( + $schedulereportempty === static::REPORT_EMPTY_DONT_SEND && + $scheduleattachment !== null && + report::get_report_row_count($schedule->get('reportid')) === 0 + ) { + $trace->output('Empty report, skipping', 1); + } else { + // Now iterate over recipient users, send the report to each. + foreach ($users as $user) { + $trace->output('Sending to: ' . fullname($user, true), 1); + + // If we already created the attachment, send that. Otherwise generate per recipient. + if ($scheduleattachment !== null) { + $this->send_message($user, $scheduleattachment); + } else { + \core\cron::setup_user($user); + + if ( + $schedulereportempty === static::REPORT_EMPTY_DONT_SEND && + report::get_report_row_count($schedule->get('reportid')) === 0 + ) { + $trace->output('Empty report, skipping', 2); + continue; + } + + $recipientattachment = helper::get_schedule_report_file($schedule); + $this->send_message($user, $recipientattachment); + $recipientattachment->delete(); + } + } + } + } + + if ($scheduleattachment !== null) { + $scheduleattachment->delete(); + } + } + + /** + * Send schedule message to user + * + * @param stdClass $user + * @param stored_file $attachment + * @return bool + */ + private function send_message(stdClass $user, stored_file $attachment): bool { + $config = $this->get_configdata(); + + $message = new \core\message\message(); + $message->component = 'moodle'; + $message->name = 'reportbuilderschedule'; + $message->courseid = SITEID; + $message->userfrom = user::get_noreply_user(); + $message->userto = $user; + $message->subject = $config['subject']; + $message->fullmessage = $config['message']['text']; + $message->fullmessageformat = $config['message']['format']; + $message->fullmessagehtml = $message->fullmessage; + $message->smallmessage = $message->fullmessage; + + // Attach report to outgoing message. + $message->attachment = $attachment; + $message->attachname = $attachment->get_filename(); + + return (bool) message_send($message); + } +} diff --git a/public/reportbuilder/classes/task/send_schedule.php b/public/reportbuilder/classes/task/send_schedule.php index ad3eff75a0c..8cf0c22b109 100644 --- a/public/reportbuilder/classes/task/send_schedule.php +++ b/public/reportbuilder/classes/task/send_schedule.php @@ -21,7 +21,8 @@ namespace core_reportbuilder\task; use core\{clock, di}; use core\task\adhoc_task; use core_user; -use core_reportbuilder\local\helpers\{report, schedule as helper}; +use core_reportbuilder\local\schedules\base; +use core_reportbuilder\local\helpers\schedule as helper; use core_reportbuilder\local\models\schedule; use moodle_exception; @@ -62,14 +63,13 @@ class send_schedule extends adhoc_task { } $schedule = schedule::get_record(['id' => $scheduleid, 'reportid' => $reportid]); - if ($schedule === false) { + if ($schedule === false || !$instance = base::from_persistent($schedule)) { $this->log('Invalid schedule', 0); return; } - $this->log_start('Sending schedule: ' . $schedule->get_formatted_name()); + $this->log_start('Sending schedule: ' . $schedule->get_formatted_name() . ' (' . $instance->get_name() . ')'); - $scheduleattachment = null; $originaluser = $USER; // Get the schedule creator, ensure it's an active account. @@ -84,71 +84,19 @@ class send_schedule extends adhoc_task { // Switch to schedule creator, and retrieve list of recipient users. \core\cron::setup_user($schedulecreator); - $users = helper::get_schedule_report_users($schedule); - if (count($users) > 0) { - - $scheduleuserviewas = $schedule->get('userviewas'); - $schedulereportempty = $schedule->get('reportempty'); - - // Handle schedule configuration as to who the report should be viewed as. - if ($scheduleuserviewas === schedule::REPORT_VIEWAS_CREATOR) { - $scheduleattachment = helper::get_schedule_report_file($schedule); - } else if ($scheduleuserviewas !== schedule::REPORT_VIEWAS_RECIPIENT) { - - // Get the user to view the schedule report as, ensure it's an active account. - try { - $scheduleviewas = core_user::get_user($scheduleuserviewas, '*', MUST_EXIST); - core_user::require_active_user($scheduleviewas); - } catch (moodle_exception $exception) { - $this->log('Invalid schedule view as user: ' . $exception->getMessage(), 0); - return; - } - - \core\cron::setup_user($scheduleviewas); - $scheduleattachment = helper::get_schedule_report_file($schedule); - } - - // Apply special handling if report is empty (default is to send it anyway). - if ($schedulereportempty === schedule::REPORT_EMPTY_DONT_SEND && $scheduleattachment !== null && - report::get_report_row_count($schedule->get('reportid')) === 0) { - - $this->log('Empty report, skipping'); - } else { - - // Now iterate over recipient users, send the report to each. - foreach ($users as $user) { - $this->log('Sending to: ' . fullname($user, true)); - - // If we already created the attachment, send that. Otherwise generate per recipient. - if ($scheduleattachment !== null) { - helper::send_schedule_message($schedule, $user, $scheduleattachment); - } else { - \core\cron::setup_user($user); - - if ($schedulereportempty === schedule::REPORT_EMPTY_DONT_SEND && - report::get_report_row_count($schedule->get('reportid')) === 0) { - - $this->log('Empty report, skipping', 2); - continue; - } - - $recipientattachment = helper::get_schedule_report_file($schedule); - helper::send_schedule_message($schedule, $user, $recipientattachment); - $recipientattachment->delete(); - } - } - } + $users = []; + if ($instance->requires_audience()) { + $users = helper::get_schedule_report_users($schedule); } + // Execute schedule type. + $instance->execute($users, $this->get_trace()); + // Finish, clean up (set persistent property manually to avoid updating it's user/time modified data). $DB->set_field($schedule::TABLE, 'timelastsent', di::get(clock::class)->time(), [ 'id' => $schedule->get('id'), ]); - if ($scheduleattachment !== null) { - $scheduleattachment->delete(); - } - $this->log_finish('Sending schedule complete'); // Restore cron user to original state. diff --git a/public/reportbuilder/templates/local/dynamictabs/schedules.mustache b/public/reportbuilder/templates/local/dynamictabs/schedules.mustache index f36bf0943b1..3df0860bd98 100644 --- a/public/reportbuilder/templates/local/dynamictabs/schedules.mustache +++ b/public/reportbuilder/templates/local/dynamictabs/schedules.mustache @@ -21,12 +21,16 @@ Example context (json): { + "menu": {}, "report": "The report content", "reportid": 10 } }}

{{#str}} schedules, core_reportbuilder {{/str}}

+ {{#menu}} + {{> core/local/dropdown/status}} + {{/menu}} {{{ report }}}
diff --git a/public/reportbuilder/tests/behat/audience.feature b/public/reportbuilder/tests/behat/audience.feature index 101bbea6373..0a066a27f59 100644 --- a/public/reportbuilder/tests/behat/audience.feature +++ b/public/reportbuilder/tests/behat/audience.feature @@ -204,6 +204,7 @@ Feature: Configure access to reports based on intended audience And I am on the "My report" "reportbuilder > Editor" page logged in as "admin" And I click on the "Schedules" dynamic tab And I press "New schedule" + And I click on "Schedule an email" "link" in the ".dropdown" "css_element" And I set the following fields in the "New schedule" "dialogue" to these values: | Name | My schedule | | Starting from | ##tomorrow 11:00## | diff --git a/public/reportbuilder/tests/behat/schedules.feature b/public/reportbuilder/tests/behat/schedules.feature index ca539b97a80..5be4927ae12 100644 --- a/public/reportbuilder/tests/behat/schedules.feature +++ b/public/reportbuilder/tests/behat/schedules.feature @@ -30,6 +30,7 @@ Feature: Manage custom report schedules And I press "Save changes" When I click on the "Schedules" dynamic tab And I press "New schedule" + And I click on "Schedule an email" "link" in the ".dropdown" "css_element" And I set the following fields in the "New schedule" "dialogue" to these values: | Name | My schedule | | Starting from | ##tomorrow 11:00## | @@ -52,6 +53,7 @@ Feature: Manage custom report schedules And I set the field "Rename audience 'All users'" to "EnglishSpanish" When I click on the "Schedules" dynamic tab And I press "New schedule" + And I click on "Schedule an email" "link" in the ".dropdown" "css_element" Then I should see "English" in the "New schedule" "dialogue" And I should not see "Spanish" in the "New schedule" "dialogue" And I click on "Cancel" "button" in the "New schedule" "dialogue" @@ -147,11 +149,22 @@ Feature: Manage custom report schedules | Name | My updated schedule | | Starting from | ##tomorrow 11:00## | | All users: All site users | 1 | + | Subject | Tell me how to win your heart | + | Body | For I haven't got a clue | + | If the report is empty | Don't send message | And I click on "Save" "button" in the "Edit schedule details" "dialogue" Then I should see "Schedule updated" And the following should exist in the "Report schedules" table: | Name | Time last sent | Time next send | Modified by | | My updated schedule | Never | ##tomorrow 11:00##%A, %d %B %Y, %H:%M## | Admin User | + And I press "Edit schedule details" action in the "My updated schedule" report row + And the following fields in the "Edit schedule details" "dialogue" match these values: + | Name | My updated schedule | + # | Starting from | ##tomorrow 11:00## | Nope, can't: MDL-86255. + | All users: All site users | 1 | + | Subject | Tell me how to win your heart | + | Body | For I haven't got a clue | + | If the report is empty | Don't send message | Scenario: Send report schedule Given the following "core_reportbuilder > Schedules" exist: diff --git a/public/reportbuilder/tests/generator/lib.php b/public/reportbuilder/tests/generator/lib.php index 8bc7432d443..7a72bce42fb 100644 --- a/public/reportbuilder/tests/generator/lib.php +++ b/public/reportbuilder/tests/generator/lib.php @@ -18,13 +18,10 @@ declare(strict_types=1); use core\{clock, di}; use core_reportbuilder\manager; -use core_reportbuilder\local\helpers\report as helper; -use core_reportbuilder\local\helpers\schedule as schedule_helper; -use core_reportbuilder\local\models\column; -use core_reportbuilder\local\models\filter; -use core_reportbuilder\local\models\report; -use core_reportbuilder\local\models\schedule; use core_reportbuilder\local\audiences\base as audience_base; +use core_reportbuilder\local\helpers\report as helper; +use core_reportbuilder\local\models\{column, filter, report, schedule}; +use core_reportbuilder\local\schedules\base as schedule_base; /** * Report builder test generator @@ -177,10 +174,9 @@ class core_reportbuilder_generator extends component_generator_base { // Default to all users if not specified, for convenience. /** @var audience_base $classname */ - $classname = $record['classname'] ?? - \core_reportbuilder\reportbuilder\audience\allusers::class; + $classname = $record['classname'] ?? \core_reportbuilder\reportbuilder\audience\allusers::class; - return ($classname)::create($record['reportid'], $record['configdata']); + return $classname::create($record['reportid'], $record['configdata']); } /** @@ -205,16 +201,14 @@ class core_reportbuilder_generator extends component_generator_base { if (!array_key_exists('format', $record)) { $record['format'] = 'csv'; } - if (!array_key_exists('subject', $record)) { - $record['subject'] = $record['name'] . ' subject'; - } - if (!array_key_exists('message', $record)) { - $record['message'] = $record['name'] . ' message'; - } if (!array_key_exists('timescheduled', $record)) { $record['timescheduled'] = usergetmidnight(di::get(clock::class)->time() + DAYSECS); } - return schedule_helper::create_schedule((object) $record); + // Default to message schedule if not specified, for convenience. + /** @var schedule_base $classname */ + $classname = $record['classname'] ?? \core_reportbuilder\reportbuilder\schedule\message::class; + + return $classname::create((object) $record)->get_persistent(); } } diff --git a/public/reportbuilder/tests/local/helpers/schedule_test.php b/public/reportbuilder/tests/local/helpers/schedule_test.php index 99fa5c7286b..11a601b8ef9 100644 --- a/public/reportbuilder/tests/local/helpers/schedule_test.php +++ b/public/reportbuilder/tests/local/helpers/schedule_test.php @@ -24,7 +24,9 @@ use core\exception\{coding_exception, invalid_parameter_exception}; use core_cohort\reportbuilder\audience\cohortmember; use core_reportbuilder_generator; use core_reportbuilder\local\models\schedule as model; +use core_reportbuilder\local\schedules\base; use core_reportbuilder\reportbuilder\audience\manual; +use core_reportbuilder\reportbuilder\schedule\message; use core_user\reportbuilder\datasource\users; /** @@ -48,6 +50,39 @@ final class schedule_test extends advanced_testcase { $this->clock = $this->mock_clock_with_frozen(1622847600); } + /** + * Data provider for {@see test_valid} + * + * @return array[] + */ + public static function valid_provider(): array { + return [ + [message::class, true], + [base::class, false], + [\core\url::class, false], + ['doesntexist', false], + ]; + } + + /** + * Test validity of given classname as a schedule type + * + * @param string $classname + * @param bool $expected + * + * @dataProvider valid_provider + */ + public function test_valid(string $classname, bool $expected): void { + $this->assertEquals($expected, schedule::valid($classname)); + } + + /** + * Test getting list of available schedules + */ + public function test_get_schedules(): void { + $this->assertContains(message::class, schedule::get_schedules()); + } + /** * Test create schedule */ @@ -64,17 +99,19 @@ final class schedule_test extends advanced_testcase { $schedule = schedule::create_schedule((object) [ 'name' => 'My schedule', 'reportid' => $report->get('id'), + 'classname' => message::class, + 'configdata' => json_encode(['subject' => 'Hello', 'message' => 'Hola']), 'format' => 'csv', - 'subject' => 'Hello', - 'message' => 'Hola', 'timescheduled' => $timescheduled, ]); + $this->assertDebuggingCalled(null, DEBUG_DEVELOPER); + $this->assertEquals('My schedule', $schedule->get('name')); $this->assertEquals($report->get('id'), $schedule->get('reportid')); + $this->assertEquals(message::class, $schedule->get('classname')); + $this->assertEquals('{"subject":"Hello","message":"Hola"}', $schedule->get('configdata')); $this->assertEquals('csv', $schedule->get('format')); - $this->assertEquals('Hello', $schedule->get('subject')); - $this->assertEquals('Hola', $schedule->get('message')); $this->assertEquals($timescheduled, $schedule->get('timescheduled')); $this->assertEquals($timescheduled, $schedule->get('timenextsend')); } @@ -263,7 +300,7 @@ final class schedule_test extends advanced_testcase { // There is only one row in the report (the only user on the site). $count = schedule::get_schedule_report_count($schedule); - $this->assertDebuggingCalled(); + $this->assertDebuggingCalled(null, DEBUG_DEVELOPER); $this->assertEquals(1, $count); } diff --git a/public/reportbuilder/tests/privacy/provider_test.php b/public/reportbuilder/tests/privacy/provider_test.php index 9868bb9b3e7..b1e456cb6cc 100644 --- a/public/reportbuilder/tests/privacy/provider_test.php +++ b/public/reportbuilder/tests/privacy/provider_test.php @@ -261,14 +261,13 @@ final class provider_test extends provider_testcase { $this->assertEquals($schedule->get_formatted_name(), $scheduledata->name); $this->assertEquals('Yes', $scheduledata->enabled); + $this->assertEquals('Schedule an email', $scheduledata->classname); + $this->assertEquals('{}', $scheduledata->configdata); $this->assertEquals('Comma separated values (.csv)', $scheduledata->format); $this->assertNotEmpty($scheduledata->timescheduled); $this->assertEquals('None', $scheduledata->recurrence); $this->assertEquals('Schedule creator', $scheduledata->userviewas); $this->assertEquals(json_encode([$audiencepersistent->get('id')]), $scheduledata->audiences); - $this->assertEquals($schedule->get('subject'), $scheduledata->subject); - $this->assertEquals(format_text($schedule->get('message'), $schedule->get('messageformat')), $scheduledata->message); - $this->assertEquals('Send message with empty report', $scheduledata->reportempty); $this->assertEquals($user->id, $scheduledata->usercreated); $this->assertEquals($user->id, $scheduledata->usermodified); $this->assertNotEmpty($scheduledata->timecreated); diff --git a/public/reportbuilder/tests/reportbuilder/schedule/message_test.php b/public/reportbuilder/tests/reportbuilder/schedule/message_test.php new file mode 100644 index 00000000000..b71772bc24f --- /dev/null +++ b/public/reportbuilder/tests/reportbuilder/schedule/message_test.php @@ -0,0 +1,194 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\reportbuilder\schedule; + +use advanced_testcase; +use core_collator; +use core_user; +use core_reportbuilder\local\filters\user; +use core_reportbuilder\local\models\schedule; +use core_reportbuilder\manager; +use core_reportbuilder\reportbuilder\audience\manual; +use core_reportbuilder_generator; +use core_reportbuilder\task\send_schedule; +use core_notes\reportbuilder\datasource\notes; +use core_user\reportbuilder\datasource\users; + +/** + * Unit tests for message schedule class + * + * @package core_reportbuilder + * @covers \core_reportbuilder\reportbuilder\schedule\message + * @copyright 2025 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +final class message_test extends advanced_testcase { + /** + * Data provider for {@see test_execute_viewas_user} + * + * @return array[] + */ + public static function execute_report_viewas_user_provider(): array { + return [ + 'View report as schedule creator' => [schedule::REPORT_VIEWAS_CREATOR, null, 'admin', 'admin'], + 'View report as schedule recipient' => [schedule::REPORT_VIEWAS_RECIPIENT, null, 'userone', 'usertwo'], + 'View report as specific user' => [null, 'userone', 'userone', 'userone'], + ]; + } + + /** + * Test executing task for a schedule with differing "View as user" configuration + * + * @param int|null $viewasuser + * @param string|null $viewasusername + * @param string $useronesees + * @param string $usertwosees + * + * @dataProvider execute_report_viewas_user_provider + */ + public function test_execute_report_viewas_user( + ?int $viewasuser, + ?string $viewasusername, + string $useronesees, + string $usertwosees + ): void { + $this->preventResetByRollback(); + $this->resetAfterTest(); + $this->setAdminUser(); + + $userone = $this->getDataGenerator()->create_user([ + 'username' => 'userone', + 'email' => 'user1@example.com', + 'firstname' => 'Zoe', + 'lastname' => 'Zebra', + ]); + $usertwo = $this->getDataGenerator()->create_user([ + 'username' => 'usertwo', + 'email' => 'user2@example.com', + 'firstname' => 'Henrietta', + 'lastname' => 'Hamster', + ]); + + /** @var core_reportbuilder_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); + + // Create a report, with a single column and condition that the current user only sees themselves. + $report = $generator->create_report(['name' => 'Myself', 'source' => users::class, 'default' => false]); + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:username']); + + $generator->create_condition(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:userselect']); + manager::get_report_from_persistent($report) + ->set_condition_values(['user:userselect_operator' => user::USER_CURRENT]); + + // Add audience/schedule for our two test users. + $audience = $generator->create_audience([ + 'reportid' => $report->get('id'), + 'classname' => manual::class, + 'configdata' => [ + 'users' => [$userone->id, $usertwo->id], + ], + ]); + + // If "View as user" isn't specified, it should be the ID of the given "View as username". + if ($viewasuser === null) { + $viewasuser = core_user::get_user_by_username($viewasusername, '*', null, MUST_EXIST)->id; + } + $schedule = $generator->create_schedule([ + 'reportid' => $report->get('id'), + 'name' => 'My schedule', + 'userviewas' => $viewasuser, + 'audiences' => json_encode([$audience->get_persistent()->get('id')]), + 'configdata' => json_encode(['subject' => 'Hi', 'message' => ['text' => 'Hi', 'format' => 1]]), + ]); + + // Send the schedule, catch emails in sink (noting the users are sorted alphabetically). + $sink = $this->redirectEmails(); + + $this->expectOutputRegex("/^Sending schedule: My schedule \(Schedule an email\)\n" . + " Sending to: " . fullname($usertwo) . "\n" . + " Sending to: " . fullname($userone) . "\n" . + "Sending schedule complete\n/"); + $sendschedule = new send_schedule(); + $sendschedule->set_custom_data(['reportid' => $report->get('id'), 'scheduleid' => $schedule->get('id')]); + $sendschedule->execute(); + + $messages = $sink->get_messages(); + $this->assertCount(2, $messages); + + $sink->close(); + + // Ensure caught messages are consistently ordered by recipient email prior to assertions. + core_collator::asort_objects_by_property($messages, 'to'); + $messages = array_values($messages); + + $messageoneattachment = self::extract_message_attachment($messages[0]->body); + $this->assertEquals($userone->email, $messages[0]->to); + $this->assertStringEndsWith("Username\n{$useronesees}\n", $messageoneattachment); + + $messagetwoattachment = self::extract_message_attachment($messages[1]->body); + $this->assertEquals($usertwo->email, $messages[1]->to); + $this->assertStringEndsWith("Username\n{$usertwosees}\n", $messagetwoattachment); + } + + /** + * Test executing for a schedule that is configured to not send empty reports + */ + public function test_execute_report_empty(): void { + $this->resetAfterTest(); + $this->setAdminUser(); + + /** @var core_reportbuilder_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); + + // Create a report that won't return any data. + $report = $generator->create_report(['name' => 'Notes', 'source' => notes::class]); + + $audience = $generator->create_audience(['reportid' => $report->get('id'), 'configdata' => []]); + $schedule = $generator->create_schedule([ + 'reportid' => $report->get('id'), + 'name' => 'My schedule', + 'audiences' => json_encode([$audience->get_persistent()->get('id')]), + 'configdata' => json_encode(['reportempty' => message::REPORT_EMPTY_DONT_SEND]), + ]); + + $this->expectOutputString("Sending schedule: My schedule (Schedule an email)\n" . + " Empty report, skipping\n" . + "Sending schedule complete\n"); + + // Execute via task. + $sendschedule = new send_schedule(); + $sendschedule->set_custom_data(['reportid' => $report->get('id'), 'scheduleid' => $schedule->get('id')]); + $sendschedule->execute(); + } + + /** + * Given a multi-part message in MIME format, return the base64 encoded attachment contained within + * + * @param string $messagebody + * @return string + */ + private static function extract_message_attachment(string $messagebody): string { + $mimepart = preg_split('/Content-Disposition: attachment; filename="My schedule.csv"\s+/m', $messagebody); + + // Extract the base64 encoded content after the "Content-Disposition" header. + preg_match_all('/^([A-Z0-9\/\+=]+)\s/im', $mimepart[1], $matches); + + return base64_decode(implode($matches[0])); + } +} diff --git a/public/reportbuilder/tests/task/send_schedule_test.php b/public/reportbuilder/tests/task/send_schedule_test.php index 6aff1393f1f..6e7250cdd70 100644 --- a/public/reportbuilder/tests/task/send_schedule_test.php +++ b/public/reportbuilder/tests/task/send_schedule_test.php @@ -38,114 +38,6 @@ use core_user\reportbuilder\datasource\users; * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class send_schedule_test extends advanced_testcase { - - /** - * Data provider for {@see test_execute_viewas_user} - * - * @return array[] - */ - public static function execute_report_viewas_user_provider(): array { - return [ - 'View report as schedule creator' => [schedule::REPORT_VIEWAS_CREATOR, null, 'admin', 'admin'], - 'View report as schedule recipient' => [schedule::REPORT_VIEWAS_RECIPIENT, null, 'userone', 'usertwo'], - 'View report as specific user' => [null, 'userone', 'userone', 'userone'], - ]; - } - - /** - * Test executing task for a schedule with differing "View as user" configuration - * - * @param int|null $viewasuser - * @param string|null $viewasusername - * @param string $useronesees - * @param string $usertwosees - * - * @dataProvider execute_report_viewas_user_provider - */ - public function test_execute_report_viewas_user( - ?int $viewasuser, - ?string $viewasusername, - string $useronesees, - string $usertwosees - ): void { - $this->preventResetByRollback(); - $this->resetAfterTest(); - $this->setAdminUser(); - - $userone = $this->getDataGenerator()->create_user([ - 'username' => 'userone', - 'email' => 'user1@example.com', - 'firstname' => 'Zoe', - 'lastname' => 'Zebra', - ]); - $usertwo = $this->getDataGenerator()->create_user([ - 'username' => 'usertwo', - 'email' => 'user2@example.com', - 'firstname' => 'Henrietta', - 'lastname' => 'Hamster', - ]); - - /** @var core_reportbuilder_generator $generator */ - $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); - - // Create a report, with a single column and condition that the current user only sees themselves. - $report = $generator->create_report(['name' => 'Myself', 'source' => users::class, 'default' => false]); - $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:username']); - - $generator->create_condition(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:userselect']); - manager::get_report_from_persistent($report) - ->set_condition_values(['user:userselect_operator' => user::USER_CURRENT]); - - // Add audience/schedule for our two test users. - $audience = $generator->create_audience([ - 'reportid' => $report->get('id'), - 'classname' => manual::class, - 'configdata' => [ - 'users' => [$userone->id, $usertwo->id], - ], - ]); - - // If "View as user" isn't specified, it should be the ID of the given "View as username". - if ($viewasuser === null) { - $viewasuser = core_user::get_user_by_username($viewasusername, '*', null, MUST_EXIST)->id; - } - $schedule = $generator->create_schedule([ - 'reportid' => $report->get('id'), - 'name' => 'My schedule', - 'userviewas' => $viewasuser, - 'audiences' => json_encode([$audience->get_persistent()->get('id')]), - ]); - - // Send the schedule, catch emails in sink (noting the users are sorted alphabetically). - $sink = $this->redirectEmails(); - - $this->expectOutputRegex("/^Sending schedule: My schedule\n" . - " Sending to: " . fullname($usertwo) . "\n" . - " Sending to: " . fullname($userone) . "\n" . - "Sending schedule complete\n/" - ); - $sendschedule = new send_schedule(); - $sendschedule->set_custom_data(['reportid' => $report->get('id'), 'scheduleid' => $schedule->get('id')]); - $sendschedule->execute(); - - $messages = $sink->get_messages(); - $this->assertCount(2, $messages); - - $sink->close(); - - // Ensure caught messages are consistently ordered by recipient email prior to assertions. - core_collator::asort_objects_by_property($messages, 'to'); - $messages = array_values($messages); - - $messageoneattachment = self::extract_message_attachment($messages[0]->body); - $this->assertEquals($userone->email, $messages[0]->to); - $this->assertStringEndsWith("Username\n{$useronesees}\n", $messageoneattachment); - - $messagetwoattachment = self::extract_message_attachment($messages[1]->body); - $this->assertEquals($usertwo->email, $messages[1]->to); - $this->assertStringEndsWith("Username\n{$usertwosees}\n", $messagetwoattachment); - } - /** * Test executing task where the schedule "View as user" is an inactive account */ @@ -166,44 +58,8 @@ final class send_schedule_test extends advanced_testcase { 'audiences' => json_encode([$audience->get_persistent()->get('id')]), ]); - $this->expectOutputRegex("/^Sending schedule: My schedule\nInvalid schedule view as user: Invalid user/"); - $sendschedule = new send_schedule(); - $sendschedule->set_custom_data(['reportid' => $report->get('id'), 'scheduleid' => $schedule->get('id')]); - $sendschedule->execute(); - } - - /** - * Test executing task for a schedule that is configured to not send empty reports - */ - public function test_execute_report_empty(): void { - $this->resetAfterTest(); - $this->setAdminUser(); - - /** @var core_reportbuilder_generator $generator */ - $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); - - // Create a report that won't return any data. - $report = $generator->create_report(['name' => 'Myself', 'source' => users::class, 'default' => false]); - - $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:username']); - $generator->create_condition(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:username']); - - manager::get_report_from_persistent($report)->set_condition_values([ - 'user:username_operator' => text::IS_EQUAL_TO, - 'user:username_value' => 'baconlettucetomato', - ]); - - $audience = $generator->create_audience(['reportid' => $report->get('id'), 'configdata' => []]); - $schedule = $generator->create_schedule([ - 'reportid' => $report->get('id'), - 'name' => 'My schedule', - 'audiences' => json_encode([$audience->get_persistent()->get('id')]), - 'reportempty' => schedule::REPORT_EMPTY_DONT_SEND, - ]); - - $this->expectOutputString("Sending schedule: My schedule\n" . - " Empty report, skipping\n" . - "Sending schedule complete\n"); + $this->expectOutputRegex("/^Sending schedule: My schedule \(Schedule an email\)\n" . + "Invalid schedule view as user: Invalid user/"); $sendschedule = new send_schedule(); $sendschedule->set_custom_data(['reportid' => $report->get('id'), 'scheduleid' => $schedule->get('id')]); $sendschedule->execute(); @@ -222,7 +78,7 @@ final class send_schedule_test extends advanced_testcase { $report = $generator->create_report(['name' => 'My report', 'source' => users::class]); $schedule = $generator->create_schedule(['reportid' => $report->get('id'), 'name' => 'My schedule']); - $this->expectOutputString("Sending schedule: My schedule\n" . + $this->expectOutputString("Sending schedule: My schedule (Schedule an email)\n" . "Sending schedule complete\n"); $sendschedule = new send_schedule(); $sendschedule->set_custom_data(['reportid' => $report->get('id'), 'scheduleid' => $schedule->get('id')]); @@ -241,7 +97,8 @@ final class send_schedule_test extends advanced_testcase { $report = $generator->create_report(['name' => 'My report', 'source' => users::class]); $schedule = $generator->create_schedule(['reportid' => $report->get('id'), 'name' => 'My schedule', 'usercreated' => 42]); - $this->expectOutputRegex("/^Sending schedule: My schedule\nInvalid schedule creator: Invalid user/"); + $this->expectOutputRegex("/^Sending schedule: My schedule \(Schedule an email\)\n" . + "Invalid schedule creator: Invalid user/"); $sendschedule = new send_schedule(); $sendschedule->set_custom_data(['reportid' => $report->get('id'), 'scheduleid' => $schedule->get('id')]); $sendschedule->execute(); @@ -263,19 +120,4 @@ final class send_schedule_test extends advanced_testcase { $sendschedule->set_custom_data(['reportid' => $report->get('id'), 'scheduleid' => 42]); $sendschedule->execute(); } - - /** - * Given a multi-part message in MIME format, return the base64 encoded attachment contained within - * - * @param string $messagebody - * @return string - */ - private static function extract_message_attachment(string $messagebody): string { - $mimepart = preg_split('/Content-Disposition: attachment; filename="My schedule.csv"\s+/m', $messagebody); - - // Extract the base64 encoded content after the "Content-Disposition" header. - preg_match_all('/^([A-Z0-9\/\+=]+)\s/im', $mimepart[1], $matches); - - return base64_decode(implode($matches[0])); - } } diff --git a/public/version.php b/public/version.php index 61b113e09e0..3eb4213d3d4 100644 --- a/public/version.php +++ b/public/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2025090200.01; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2025090200.02; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes. $release = '5.1dev+ (Build: 20250902)'; // Human-friendly version name