From c439e2d3c1cd6a79e0fad4b681d175070c5ae6ec Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Thu, 27 Feb 2025 13:31:38 +0000 Subject: [PATCH] MDL-82936 reportbuilder: new APIs for enhancing report rendering. We're introducing two new sets of APIs for both custom and system reports: * Report actions allow for the definition of an action to belong to the report and be rendered alongside the filters button; * Report info container allows for the definition of content to render between the aforementioned action buttons and the table content itself --- .upgradenotes/MDL-82936-2025022811045697.yml | 20 ++++ .../external/custom_report_exporter.php | 25 +++-- .../external/report_action_exporter.php | 106 ++++++++++++++++++ .../external/system_report_exporter.php | 14 ++- reportbuilder/classes/local/report/base.php | 46 +++++++- reportbuilder/classes/output/renderer.php | 5 + .../classes/output/report_action.php | 57 ++++++++++ reportbuilder/templates/report.mustache | 27 +++-- .../external/custom_report_exporter_test.php | 21 +++- .../tests/external/reports/get_test.php | 28 +++-- .../external/system_report_exporter_test.php | 16 ++- 11 files changed, 330 insertions(+), 35 deletions(-) create mode 100644 .upgradenotes/MDL-82936-2025022811045697.yml create mode 100644 reportbuilder/classes/external/report_action_exporter.php create mode 100644 reportbuilder/classes/output/report_action.php diff --git a/.upgradenotes/MDL-82936-2025022811045697.yml b/.upgradenotes/MDL-82936-2025022811045697.yml new file mode 100644 index 00000000000..52d0a845c19 --- /dev/null +++ b/.upgradenotes/MDL-82936-2025022811045697.yml @@ -0,0 +1,20 @@ +issueNumber: MDL-82936 +notes: + core_reportbuilder: + - message: >- + The `render_new_report_button` method of the `core_reportbuilder` + renderer has been deprecated. Instead, refer to the report instance + `set_report_action` method + type: deprecated + - message: >- + The base report class, used by both `\core_reportbuilder\system_report` + and `\core_reportbuilder\datasource`, contains new methods for enhancing + report rendering + + + * `set_report_action` allows for an action button to belong to your + report, and be rendered alongside the filters button; + + * `set_report_info_container` allows for content to be rendered by your + report, between the action buttons and the table content + type: improved diff --git a/reportbuilder/classes/external/custom_report_exporter.php b/reportbuilder/classes/external/custom_report_exporter.php index 18b6b05d21d..ee28deb659d 100644 --- a/reportbuilder/classes/external/custom_report_exporter.php +++ b/reportbuilder/classes/external/custom_report_exporter.php @@ -90,6 +90,11 @@ class custom_report_exporter extends persistent_exporter { protected static function define_other_properties(): array { return [ 'table' => ['type' => PARAM_RAW], + 'button' => [ + 'type' => report_action_exporter::read_properties_definition(), + 'optional' => true, + ], + 'infocontainer' => ['type' => PARAM_RAW], 'filtersapplied' => ['type' => PARAM_INT], 'filterspresent' => ['type' => PARAM_BOOL], 'filtersform' => ['type' => PARAM_RAW], @@ -136,6 +141,7 @@ class custom_report_exporter extends persistent_exporter { /** @var datasource $report */ $report = manager::get_report_from_persistent($this->persistent); + $optionalvalues = []; $filterspresent = false; $filtersform = ''; $attributes = []; @@ -151,6 +157,11 @@ class custom_report_exporter extends persistent_exporter { $table = custom_report_table_view::create($this->persistent->get('id'), $this->download); $table->set_filterset($filterset); + // Export global report action. + if ($reportaction = $report->get_report_action()) { + $optionalvalues['button'] = $reportaction->export_for_template($output); + } + // Generate filters form if report contains any filters. $filterspresent = !empty($report->get_active_filters()); if ($filterspresent && empty($this->download)) { @@ -169,26 +180,26 @@ class custom_report_exporter extends persistent_exporter { } // If we are editing we need all this information for the template. - $editordata = []; if ($this->editmode) { $menucardsexporter = new custom_report_column_cards_exporter(null, ['report' => $report]); - $editordata['sidebarmenucards'] = (array) $menucardsexporter->export($output); + $optionalvalues['sidebarmenucards'] = (array) $menucardsexporter->export($output); $conditionsexporter = new custom_report_conditions_exporter(null, ['report' => $report]); - $editordata['conditions'] = (array) $conditionsexporter->export($output); + $optionalvalues['conditions'] = (array) $conditionsexporter->export($output); $filtersexporter = new custom_report_filters_exporter(null, ['report' => $report]); - $editordata['filters'] = (array) $filtersexporter->export($output); + $optionalvalues['filters'] = (array) $filtersexporter->export($output); $sortingexporter = new custom_report_columns_sorting_exporter(null, ['report' => $report]); - $editordata['sorting'] = (array) $sortingexporter->export($output); + $optionalvalues['sorting'] = (array) $sortingexporter->export($output); $cardviewexporter = new custom_report_card_view_exporter(null, ['report' => $report]); - $editordata['cardview'] = (array) $cardviewexporter->export($output); + $optionalvalues['cardview'] = (array) $cardviewexporter->export($output); } return [ 'table' => $output->render($table), + 'infocontainer' => $report->get_report_info_container(), 'filtersapplied' => $report->get_applied_filter_count(), 'filterspresent' => $filterspresent, 'filtersform' => $filtersform, @@ -196,7 +207,7 @@ class custom_report_exporter extends persistent_exporter { 'classes' => $classes ?? '', 'editmode' => $this->editmode, 'javascript' => '', - ] + $editordata; + ] + $optionalvalues; } /** diff --git a/reportbuilder/classes/external/report_action_exporter.php b/reportbuilder/classes/external/report_action_exporter.php new file mode 100644 index 00000000000..b9e310f0ae3 --- /dev/null +++ b/reportbuilder/classes/external/report_action_exporter.php @@ -0,0 +1,106 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\external; + +use core\context\system; +use core\external\exporter; +use core\output\renderer_base; +use core_reportbuilder\output\report_action; + +/** + * Encapsulate a report action + * + * @package core_reportbuilder + * @copyright 2025 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class report_action_exporter extends exporter { + + /** + * Return a list of objects that are related to the exporter + * + * @return array + */ + protected static function define_related(): array { + return [ + 'reportaction' => report_action::class, + ]; + } + + /** + * Return the list of additional properties for read structure and export + * + * @return array[] + */ + protected static function define_other_properties(): array { + return [ + 'tag' => [ + 'type' => PARAM_ALPHA, + ], + 'title' => [ + 'type' => PARAM_TEXT, + ], + 'attributes' => [ + 'type' => [ + 'name' => [ + 'type' => PARAM_RAW, + 'optional' => true, + ], + 'value' => [ + 'type' => PARAM_RAW, + 'optional' => true, + ], + ], + ], + ]; + } + + /** + * Return text formatting parameters for title property + * + * @return array + */ + protected function get_format_parameters_for_title(): array { + return [ + 'context' => system::instance(), + ]; + } + + /** + * Get the additional values to inject while exporting + * + * @param renderer_base $output + * @return array + */ + protected function get_other_values(renderer_base $output): array { + + /** @var report_action $reportaction */ + $reportaction = $this->related['reportaction']; + + $attributes = array_map(static function($key, $value): array { + return ['name' => $key, 'value' => $value]; + }, array_keys($reportaction->attributes), $reportaction->attributes); + + return [ + 'tag' => $reportaction->tag ?: 'button', + 'title' => $reportaction->title, + 'attributes' => $attributes, + ]; + } +} diff --git a/reportbuilder/classes/external/system_report_exporter.php b/reportbuilder/classes/external/system_report_exporter.php index 919c58cc1c2..006f9683ad3 100644 --- a/reportbuilder/classes/external/system_report_exporter.php +++ b/reportbuilder/classes/external/system_report_exporter.php @@ -67,6 +67,11 @@ class system_report_exporter extends persistent_exporter { return [ 'table' => ['type' => PARAM_RAW], 'parameters' => ['type' => PARAM_RAW], + 'button' => [ + 'type' => report_action_exporter::read_properties_definition(), + 'optional' => true, + ], + 'infocontainer' => ['type' => PARAM_RAW], 'filterspresent' => ['type' => PARAM_BOOL], 'filtersapplied' => ['type' => PARAM_INT], 'filtersform' => ['type' => PARAM_RAW], @@ -108,6 +113,12 @@ class system_report_exporter extends persistent_exporter { $table = system_report_table::create($reportid, $params); $table->set_filterset($filterset); + // Export global report action. + $optionalvalues = []; + if ($reportaction = $source->get_report_action()) { + $optionalvalues['button'] = $reportaction->export_for_template($output); + } + // Generate filters form if report uses the default form, and contains any filters. $filterspresent = $source->get_filter_form_default() && !empty($source->get_active_filters()); if ($filterspresent && empty($params['download'])) { @@ -131,11 +142,12 @@ class system_report_exporter extends persistent_exporter { return [ 'table' => $output->render($table), 'parameters' => $parameters, + 'infocontainer' => $source->get_report_info_container(), 'filterspresent' => $filterspresent, 'filtersapplied' => $source->get_applied_filter_count(), 'filtersform' => $filterspresent ? $filtersform->render() : '', 'attributes' => $attributes, 'classes' => $classes ?? '', - ]; + ] + $optionalvalues; } } diff --git a/reportbuilder/classes/local/report/base.php b/reportbuilder/classes/local/report/base.php index 0a335f78bfe..3bd3d5c6ff0 100644 --- a/reportbuilder/classes/local/report/base.php +++ b/reportbuilder/classes/local/report/base.php @@ -23,9 +23,9 @@ use context; use lang_string; use core_reportbuilder\local\entities\base as entity_base; use core_reportbuilder\local\filters\base as filter_base; -use core_reportbuilder\local\helpers\database; -use core_reportbuilder\local\helpers\user_filter_manager; +use core_reportbuilder\local\helpers\{database, user_filter_manager}; use core_reportbuilder\local\models\report; +use core_reportbuilder\output\report_action; /** * Base class for all reports @@ -87,6 +87,12 @@ abstract class base { /** @var int Default paging size */ private $defaultperpage = self::DEFAULT_PAGESIZE; + /** @var report_action $reportaction */ + private report_action|null $reportaction = null; + + /** @var string $reportinfocontainer */ + private string $reportinfocontainer = ''; + /** @var array $attributes */ private $attributes = []; @@ -882,6 +888,42 @@ abstract class base { return $this->defaultperpage; } + /** + * Sets the report action to be rendered above the table + * + * @param report_action $reportaction + */ + final public function set_report_action(report_action $reportaction): void { + $this->reportaction = $reportaction; + } + + /** + * Gets the report action to be rendered abover the table + * + * @return report_action|null + */ + final public function get_report_action(): ?report_action { + return $this->reportaction; + } + + /** + * Sets the report info container content to be rendered between action buttons and table + * + * @param string $reportinfocontainer + */ + final public function set_report_info_container(string $reportinfocontainer): void { + $this->reportinfocontainer = $reportinfocontainer; + } + + /** + * Gets the report info container content to be rendered between action buttons and table + * + * @return string + */ + final public function get_report_info_container(): string { + return $this->reportinfocontainer; + } + /** * Add report attributes (data-, class, etc.) that will be included in HTML when report is displayed * diff --git a/reportbuilder/classes/output/renderer.php b/reportbuilder/classes/output/renderer.php index f602d59858f..9be567d0416 100644 --- a/reportbuilder/classes/output/renderer.php +++ b/reportbuilder/classes/output/renderer.php @@ -108,8 +108,13 @@ class renderer extends plugin_renderer_base { * Renders the New report button * * @return string + * + * @deprecated since Moodle 5.0 - please use {@see \core_reportbuilder\system_report::set_report_action} instead */ + #[\core\attribute\deprecated('\core_reportbuilder\system_report::set_report_action', mdl: 'MDL-82936', since: '5.0')] public function render_new_report_button(): string { + \core\deprecation::emit_deprecation_if_present([self::class, __FUNCTION__]); + return html_writer::tag('button', get_string('newreport', 'core_reportbuilder'), [ 'class' => 'btn btn-primary my-auto', 'data-action' => 'report-create', diff --git a/reportbuilder/classes/output/report_action.php b/reportbuilder/classes/output/report_action.php new file mode 100644 index 00000000000..652cb746c32 --- /dev/null +++ b/reportbuilder/classes/output/report_action.php @@ -0,0 +1,57 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\output; + +use core\output\{renderer_base, templatable}; +use core_reportbuilder\external\report_action_exporter; + +/** + * Encapsulate a report action + * + * @package core_reportbuilder + * @copyright 2025 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class report_action implements templatable { + + /** + * Constructor + * + * @param string $title + * @param array $attributes + * @param string $tag + */ + public function __construct( + /** @var string */ + public readonly string $title, + /** @var array */ + public readonly array $attributes, + /** @var string */ + public readonly string $tag = 'button', + ) { + + } + + #[\Override] + public function export_for_template(renderer_base $output): array { + $exporter = new report_action_exporter(null, ['reportaction' => $this]); + + return (array) $exporter->export($output); + } +} diff --git a/reportbuilder/templates/report.mustache b/reportbuilder/templates/report.mustache index 65be77cb595..729df86b4a8 100644 --- a/reportbuilder/templates/report.mustache +++ b/reportbuilder/templates/report.mustache @@ -30,6 +30,15 @@ "value": "1" }], "table": "table", + "button": { + "tag": "button", + "title": "Click me", + "attributes": [{ + "name": "class", + "value": "btn btn-primary" + }] + }, + "infocontainer": "Here's some information", "filterspresent": true, "filtersform": "form" } @@ -41,15 +50,19 @@ data-parameter="{{parameters}}" {{#attributes}}{{name}}="{{value}}" {{/attributes}}>
- {{#filterspresent}} -
diff --git a/reportbuilder/tests/external/custom_report_exporter_test.php b/reportbuilder/tests/external/custom_report_exporter_test.php index e1fd3fa00ca..9bee6e88dfa 100644 --- a/reportbuilder/tests/external/custom_report_exporter_test.php +++ b/reportbuilder/tests/external/custom_report_exporter_test.php @@ -24,6 +24,7 @@ use core_reportbuilder_generator; use moodle_url; use core_reportbuilder\local\helpers\user_filter_manager; use core_reportbuilder\local\filters\text; +use core_reportbuilder\output\report_action; use core_user\reportbuilder\datasource\users; /** @@ -47,7 +48,11 @@ final class custom_report_exporter_test extends advanced_testcase { /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'My report', 'source' => users::class, 'default' => false]); - manager::get_report_from_persistent($report)->add_attributes(['data-foo' => 'bar', 'data-another' => '1']); + + $instance = manager::get_report_from_persistent($report); + $instance->set_report_action(new report_action('Add', [])); + $instance->set_report_info_container('Hello'); + $instance->add_attributes(['data-foo' => 'bar', 'data-another' => '1']); $PAGE->set_url(new moodle_url('/')); @@ -55,6 +60,7 @@ final class custom_report_exporter_test extends advanced_testcase { $export = $exporter->export($PAGE->get_renderer('core_reportbuilder')); $this->assertNotEmpty($export->table); + $this->assertEquals('Hello', $export->infocontainer); $this->assertEquals(0, $export->filtersapplied); $this->assertFalse($export->filterspresent); $this->assertEmpty($export->filtersform); @@ -62,6 +68,7 @@ final class custom_report_exporter_test extends advanced_testcase { $this->assertEmpty($export->attributes); // The following are all generated by additional exporters. + $this->assertEmpty($export->button); $this->assertNotEmpty($export->sidebarmenucards); $this->assertNotEmpty($export->conditions); $this->assertNotEmpty($export->filters); @@ -80,7 +87,11 @@ final class custom_report_exporter_test extends advanced_testcase { /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'My report', 'source' => users::class, 'default' => false]); - manager::get_report_from_persistent($report)->add_attributes(['data-foo' => 'bar', 'data-another' => '1']); + + $instance = manager::get_report_from_persistent($report); + $instance->set_report_action(new report_action('Add', [])); + $instance->set_report_info_container('Hello'); + $instance->add_attributes(['data-foo' => 'bar', 'data-another' => '1']); $PAGE->set_url(new moodle_url('/')); @@ -88,6 +99,7 @@ final class custom_report_exporter_test extends advanced_testcase { $export = $exporter->export($PAGE->get_renderer('core_reportbuilder')); $this->assertNotEmpty($export->table); + $this->assertEquals('Hello', $export->infocontainer); $this->assertEquals(0, $export->filtersapplied); $this->assertFalse($export->filterspresent); $this->assertEmpty($export->filtersform); @@ -97,7 +109,10 @@ final class custom_report_exporter_test extends advanced_testcase { ['name' => 'data-another', 'value' => '1'] ], $export->attributes); - // The following are all generated by additional exporters, and should not be present when not editing. + // The following are all generated by additional exporters. + $this->assertNotEmpty($export->button); + + // The following should not be present when not editing. $this->assertObjectNotHasProperty('sidebarmenucards', $export); $this->assertObjectNotHasProperty('conditions', $export); $this->assertObjectNotHasProperty('filters', $export); diff --git a/reportbuilder/tests/external/reports/get_test.php b/reportbuilder/tests/external/reports/get_test.php index 01e2ae28e50..d1c403a1565 100644 --- a/reportbuilder/tests/external/reports/get_test.php +++ b/reportbuilder/tests/external/reports/get_test.php @@ -23,6 +23,8 @@ use core_reportbuilder_generator; use core_external\external_api; use externallib_advanced_testcase; use core_reportbuilder\exception\report_access_exception; +use core_reportbuilder\manager; +use core_reportbuilder\output\report_action; use core_user\reportbuilder\datasource\users; defined('MOODLE_INTERNAL') || die(); @@ -49,12 +51,11 @@ final class get_test extends externallib_advanced_testcase { /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); + $report = $generator->create_report(['name' => 'My report', 'source' => users::class, 'default' => false]); - $report = $generator->create_report([ - 'name' => 'My report', - 'source' => users::class, - 'default' => false, - ]); + $instance = manager::get_report_from_persistent($report); + $instance->set_report_action(new report_action('Add', [])); + $instance->set_report_info_container('Hello'); // Add two filters. $filterfullname = $generator->create_filter(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:fullname']); @@ -68,6 +69,8 @@ final class get_test extends externallib_advanced_testcase { $this->assertEquals($result['source'], users::class); $this->assertNotEmpty($result['table']); $this->assertNotEmpty($result['javascript']); + $this->assertEmpty($result['button']); + $this->assertEquals('Hello', $result['infocontainer']); $this->assertFalse($result['filterspresent']); $this->assertEmpty($result['filtersform']); $this->assertTrue($result['editmode']); @@ -94,12 +97,11 @@ final class get_test extends externallib_advanced_testcase { /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); + $report = $generator->create_report(['name' => 'My report', 'source' => users::class, 'default' => false]); - $report = $generator->create_report([ - 'name' => 'My report', - 'source' => users::class, - 'default' => false, - ]); + $instance = manager::get_report_from_persistent($report); + $instance->set_report_action(new report_action('Add', [])); + $instance->set_report_info_container('Hello'); // Add two filters. $generator->create_filter(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:fullname']); @@ -113,6 +115,12 @@ final class get_test extends externallib_advanced_testcase { $this->assertEquals($result['source'], users::class); $this->assertNotEmpty($result['table']); $this->assertNotEmpty($result['javascript']); + $this->assertEquals([ + 'tag' => 'button', + 'title' => 'Add', + 'attributes' => [], + ], $result['button']); + $this->assertEquals('Hello', $result['infocontainer']); $this->assertTrue($result['filterspresent']); $this->assertNotEmpty($result['filtersform']); $this->assertFalse($result['editmode']); diff --git a/reportbuilder/tests/external/system_report_exporter_test.php b/reportbuilder/tests/external/system_report_exporter_test.php index ac15d7b902f..4a45357c28f 100644 --- a/reportbuilder/tests/external/system_report_exporter_test.php +++ b/reportbuilder/tests/external/system_report_exporter_test.php @@ -21,6 +21,7 @@ namespace core_reportbuilder\external; use advanced_testcase; use context_system; use moodle_url; +use core_reportbuilder\output\report_action; use core_reportbuilder\system_report_available; use core_reportbuilder\system_report_factory; @@ -71,16 +72,21 @@ final class system_report_exporter_test extends advanced_testcase { // Prevent debug warnings from flexible_table. $PAGE->set_url(new moodle_url('/')); - $systemreport = system_report_factory::create(system_report_available::class, context_system::instance(), '', '', 0, - ['withfilters' => $withfilters])->add_attributes(['data-foo' => 'bar', 'data-another' => '1']); + $instance = system_report_factory::create(system_report_available::class, context_system::instance(), '', '', 0, + ['withfilters' => $withfilters]); + $instance->set_report_action(new report_action('Add', [])); + $instance->set_report_info_container('Hello'); + $instance->add_attributes(['data-foo' => 'bar', 'data-another' => '1']); - $exporter = new system_report_exporter($systemreport->get_report_persistent(), [ - 'source' => $systemreport, - 'parameters' => json_encode($systemreport->get_parameters()), + $exporter = new system_report_exporter($instance->get_report_persistent(), [ + 'source' => $instance, + 'parameters' => json_encode($instance->get_parameters()), ]); $data = $exporter->export($PAGE->get_renderer('core_reportbuilder')); $this->assertNotEmpty($data->table); + $this->assertNotEmpty($data->button); + $this->assertEquals('Hello', $data->infocontainer); if ($withfilters) { $this->assertEquals('{"withfilters":true}', $data->parameters); -- 2.43.0