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
This commit is contained in:
Paul Holden 2025-02-27 13:31:38 +00:00
parent 5670447ece
commit c439e2d3c1
No known key found for this signature in database
GPG Key ID: A81A96D6045F6164
11 changed files with 330 additions and 35 deletions

View File

@ -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

View File

@ -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;
}
/**

View File

@ -0,0 +1,106 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
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 <paulh@moodle.com>
* @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,
];
}
}

View File

@ -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;
}
}

View File

@ -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
*

View File

@ -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',

View File

@ -0,0 +1,57 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
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 <paulh@moodle.com>
* @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);
}
}

View File

@ -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}}>
<div class="reportbuilder-wrapper">
{{#filterspresent}}
<div class="dropdown d-flex justify-content-end">
{{! Action buttons }}
<div class="dropdown d-flex justify-content-end mb-3">
{{#button}}
<{{{tag}}} {{#attributes}}{{name}}="{{value}}" {{/attributes}}>{{title}}</{{{tag}}}>
{{/button}}
{{#filterspresent}}
{{>core_reportbuilder/local/filters/area}}
</div>
{{/filterspresent}}
{{! Table }}
<div class="mt-2">
{{{table}}}
{{/filterspresent}}
</div>
{{! Info container }}
{{#infocontainer}}<div class="mt-2">{{{.}}}</div>{{/infocontainer}}
{{! Table }}
<div class="mt-2">{{{table}}}</div>
</div>
</div>

View File

@ -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);

View File

@ -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']);

View File

@ -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);