From fc611855143b54d00d174b14f401fe22eab6ed4e Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Mon, 18 Aug 2025 21:37:10 +0100 Subject: [PATCH 1/3] MDL-86066 reportbuilder: schedule schema update to support class types. In order to support upcoming changes that allow for multiple types of report schedule to be configured, update the table/persistent schema to be more generic. Audience data is retained as a first-class property, in order to more easily and consistently support schedules that make use of audience data and API, although the field itself is now nullable. --- public/lib/db/install.xml | 8 +-- public/lib/db/upgrade.php | 66 ++++++++++++++++++- .../classes/local/helpers/audience.php | 2 +- .../classes/local/helpers/report.php | 2 +- .../classes/local/helpers/schedule.php | 2 +- .../classes/local/models/schedule.php | 35 +++------- public/reportbuilder/tests/generator/lib.php | 6 -- .../tests/local/helpers/schedule_test.php | 9 +-- public/version.php | 2 +- 9 files changed, 86 insertions(+), 46 deletions(-) diff --git a/public/lib/db/install.xml b/public/lib/db/install.xml index 998bead6b22..a39a6d55f32 100644 --- a/public/lib/db/install.xml +++ b/public/lib/db/install.xml @@ -4666,15 +4666,13 @@ - + + + - - - - diff --git a/public/lib/db/upgrade.php b/public/lib/db/upgrade.php index 0fbad8bb7dd..e5cc09331fd 100644 --- a/public/lib/db/upgrade.php +++ b/public/lib/db/upgrade.php @@ -1302,7 +1302,6 @@ function xmldb_main_upgrade($oldversion) { upgrade_main_savepoint(true, 2024112900.02); } - // Moodle 5.0 Upgrade. if ($oldversion < 2024121800.00) { $smsgateways = $DB->get_records('sms_gateways'); foreach ($smsgateways as $gateway) { @@ -2082,5 +2081,70 @@ function xmldb_main_upgrade($oldversion) { upgrade_main_savepoint(true, 2025082600.01); } + if ($oldversion < 2025082900.01) { + $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, 2025082900.01); + } + return true; } 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..035fec257cf 100644 --- a/public/reportbuilder/classes/local/helpers/schedule.php +++ b/public/reportbuilder/classes/local/helpers/schedule.php @@ -113,7 +113,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); diff --git a/public/reportbuilder/classes/local/models/schedule.php b/public/reportbuilder/classes/local/models/schedule.php index d468d5ec2f3..82c0703fdca 100644 --- a/public/reportbuilder/classes/local/models/schedule.php +++ b/public/reportbuilder/classes/local/models/schedule.php @@ -95,27 +95,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 +128,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/tests/generator/lib.php b/public/reportbuilder/tests/generator/lib.php index 8bc7432d443..c403b1ba640 100644 --- a/public/reportbuilder/tests/generator/lib.php +++ b/public/reportbuilder/tests/generator/lib.php @@ -205,12 +205,6 @@ 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); } diff --git a/public/reportbuilder/tests/local/helpers/schedule_test.php b/public/reportbuilder/tests/local/helpers/schedule_test.php index 99fa5c7286b..1acb87a4442 100644 --- a/public/reportbuilder/tests/local/helpers/schedule_test.php +++ b/public/reportbuilder/tests/local/helpers/schedule_test.php @@ -25,6 +25,7 @@ use core_cohort\reportbuilder\audience\cohortmember; use core_reportbuilder_generator; use core_reportbuilder\local\models\schedule as model; use core_reportbuilder\reportbuilder\audience\manual; +use core_reportbuilder\reportbuilder\schedule\message; use core_user\reportbuilder\datasource\users; /** @@ -64,17 +65,17 @@ 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->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')); } diff --git a/public/version.php b/public/version.php index b5d6861442a..da09e5a0faa 100644 --- a/public/version.php +++ b/public/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2025082900.00; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2025082900.01; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes. $release = '5.1dev+ (Build: 20250829)'; // Human-friendly version name From ce09276d3e7085f064fc620018fad942f22d7224 Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Wed, 20 Aug 2025 15:10:55 +0100 Subject: [PATCH 2/3] MDL-86066 reportbuilder: establish base schedule class implementation. Move previous functionality from, and deprecate, the schedule helper class method for creation. Switch task to support new API, and fix privacy provider for the same. --- .upgradenotes/MDL-86066-2025082110031090.yml | 19 ++ public/lang/en/deprecated.txt | 3 + public/lang/en/reportbuilder.php | 12 +- .../classes/local/helpers/schedule.php | 22 +- .../classes/local/schedules/base.php | 176 ++++++++++++++++ .../classes/privacy/provider.php | 57 ++--- .../reportbuilder/schedule/message.php | 178 ++++++++++++++++ .../classes/task/send_schedule.php | 72 +------ public/reportbuilder/tests/generator/lib.php | 20 +- .../tests/local/helpers/schedule_test.php | 38 +++- .../tests/privacy/provider_test.php | 5 +- .../reportbuilder/schedule/message_test.php | 194 ++++++++++++++++++ .../tests/task/send_schedule_test.php | 168 +-------------- 13 files changed, 691 insertions(+), 273 deletions(-) create mode 100644 .upgradenotes/MDL-86066-2025082110031090.yml create mode 100644 public/reportbuilder/classes/local/schedules/base.php create mode 100644 public/reportbuilder/classes/reportbuilder/schedule/message.php create mode 100644 public/reportbuilder/tests/reportbuilder/schedule/message_test.php 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/reportbuilder/classes/local/helpers/schedule.php b/public/reportbuilder/classes/local/helpers/schedule.php index 035fec257cf..5d9657cf00a 100644 --- a/public/reportbuilder/classes/local/helpers/schedule.php +++ b/public/reportbuilder/classes/local/helpers/schedule.php @@ -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,15 @@ 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); + } /** * Create report schedule, calculate when it should be next sent @@ -46,18 +56,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(); } /** 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/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/tests/generator/lib.php b/public/reportbuilder/tests/generator/lib.php index c403b1ba640..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']); } /** @@ -209,6 +205,10 @@ class core_reportbuilder_generator extends component_generator_base { $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 1acb87a4442..11a601b8ef9 100644 --- a/public/reportbuilder/tests/local/helpers/schedule_test.php +++ b/public/reportbuilder/tests/local/helpers/schedule_test.php @@ -24,6 +24,7 @@ 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; @@ -49,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 */ @@ -71,6 +105,8 @@ final class schedule_test extends advanced_testcase { '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')); @@ -264,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])); - } } From 513a32f94ac2e0e5d9ada698ca17b9de7a6ffaf2 Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Thu, 21 Aug 2025 09:31:41 +0100 Subject: [PATCH 3/3] MDL-86066 reportbuilder: hook up front end elements to new schedules. --- .../amd/build/local/repository/modals.min.js | 2 +- .../build/local/repository/modals.min.js.map | 2 +- .../reportbuilder/amd/build/schedules.min.js | 2 +- .../amd/build/schedules.min.js.map | 2 +- .../amd/src/local/repository/modals.js | 4 +- public/reportbuilder/amd/src/schedules.js | 5 +- .../reportbuilder/classes/form/schedule.php | 143 ++++++++++-------- .../classes/local/helpers/schedule.php | 29 +++- .../classes/local/models/schedule.php | 9 -- .../local/systemreports/report_schedules.php | 29 ++-- .../classes/output/dynamictabs/schedules.php | 37 ++++- .../local/dynamictabs/schedules.mustache | 4 + .../tests/behat/audience.feature | 1 + .../tests/behat/schedules.feature | 13 ++ 14 files changed, 186 insertions(+), 96 deletions(-) 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/schedule.php b/public/reportbuilder/classes/local/helpers/schedule.php index 5d9657cf00a..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; @@ -50,6 +50,17 @@ class schedule { 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 * @@ -327,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'; @@ -412,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 82c0703fdca..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. * 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/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: