MDL-83552 reportbuilder: Add custom fields to Report builder

This commit is contained in:
David Carrillo
2025-03-10 08:21:10 +01:00
parent 8f9a5f131e
commit c03f47abfd
18 changed files with 372 additions and 18 deletions
+12
View File
@@ -64,3 +64,15 @@ $settings->add(new admin_setting_configcheckbox(
new lang_string('customreportsliveediting_desc', 'core_reportbuilder'), 1));
$ADMIN->add('reportbuilder', $settings);
$ADMIN->add(
'reportbuilder', new accesscallback(
'reportbuildercustomfields',
get_string('reportbuildercustomfields', 'core_reportbuilder'),
(new moodle_url('/reportbuilder/customfield.php'))->out(),
static function(): bool {
return has_capability('moodle/reportbuilder:configurecustomfields', context_system::instance());
},
empty($CFG->enablecustomreports)
)
);
+1
View File
@@ -240,6 +240,7 @@ $string['renameaudience'] = 'Rename audience \'{$a}\'';
$string['renamecolumn'] = 'Rename column \'{$a}\'';
$string['renamefilter'] = 'Rename filter \'{$a}\'';
$string['reportbuilder'] = 'Report builder';
$string['reportbuildercustomfields'] = 'Custom report fields';
$string['reportcreated'] = 'Report created';
$string['reportdeleted'] = 'Report deleted';
$string['reports'] = 'Reports';
+1
View File
@@ -392,6 +392,7 @@ $string['rating:rate'] = 'Add ratings to items';
$string['rating:view'] = 'View the total rating you received';
$string['rating:viewany'] = 'View total ratings that anyone received';
$string['rating:viewall'] = 'View all raw ratings given by individuals';
$string['reportbuilder:configurecustomfields'] = 'Configure custom report fields';
$string['reportbuilder:edit'] = 'Edit your own custom reports';
$string['reportbuilder:editall'] = 'Edit all custom reports';
$string['reportbuilder:scheduleviewas'] = 'Schedule reports to be viewed as other users';
+8
View File
@@ -2749,6 +2749,14 @@ $capabilities = array(
'archetypes' => [],
],
// Allow users to configure custom fields for custom reports.
'moodle/reportbuilder:configurecustomfields' => [
'captype' => 'write',
'riskbitmap' => RISK_PERSONAL,
'contextlevel' => CONTEXT_SYSTEM,
'archetypes' => [],
],
// Allow users to schedule reports as other users.
'moodle/reportbuilder:scheduleviewas' => [
'captype' => 'read',
+3
View File
@@ -273,6 +273,9 @@ class phpunit_util extends testing_util {
if (class_exists('\core_group\customfield\grouping_handler')) {
\core_group\customfield\grouping_handler::reset_caches();
}
if (class_exists('\core_reportbuilder\customfield\report_handler')) {
\core_reportbuilder\customfield\report_handler::reset_caches();
}
// Clear static cache within restore.
if (class_exists('restore_section_structure_step')) {
@@ -0,0 +1,139 @@
<?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\customfield;
use core\context;
use core\context\system;
use core\exception\coding_exception;
use core\url;
use core_customfield\field_controller;
use core_reportbuilder\local\models\report;
use core_reportbuilder\manager;
use core_reportbuilder\permission;
/**
* Report handler for custom fields
*
* @package core_reportbuilder
* @copyright 2025 David Carrillo <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class report_handler extends \core_customfield\handler {
/**
* @var report_handler|null
*/
protected static ?report_handler $singleton = null;
/**
* Returns a singleton
*
* @param int $itemid
* @return self
*/
public static function create(int $itemid = 0): self {
if (static::$singleton === null) {
self::$singleton = new static($itemid);
}
return self::$singleton;
}
/**
* Run reset code after unit tests to reset the singleton usage.
*/
public static function reset_caches(): void {
if (!PHPUNIT_TEST) {
throw new coding_exception('This feature is only intended for use in unit tests');
}
static::$singleton = null;
}
/**
* The current user can configure custom fields on this component.
*
* @return bool true if the current can configure custom fields, false otherwise
*/
public function can_configure(): bool {
return has_capability('moodle/reportbuilder:configurecustomfields', $this->get_configuration_context());
}
/**
* The current user can edit custom fields on the given report.
*
* @param field_controller $field
* @param int $instanceid id of the report to test edit permission
* @return bool true if the current can edit custom fields, false otherwise
*/
public function can_edit(field_controller $field, int $instanceid = 0): bool {
if ($instanceid > 0) {
$report = report::get_record(['id' => $instanceid], MUST_EXIST);
return permission::can_edit_report($report);
} else {
return permission::can_create_report();
}
}
/**
* The current user can view custom fields on the given report.
*
* @param field_controller $field
* @param int $instanceid id of the report to test edit permission
* @return bool true if the current can edit custom fields, false otherwise
*/
public function can_view(field_controller $field, int $instanceid): bool {
if ($instanceid > 0) {
$report = report::get_record(['id' => $instanceid], MUST_EXIST);
return permission::can_view_report($report);
}
return permission::can_view_reports_list();
}
/**
* Context that should be used for new categories created by this handler
*
* @return context the context for configuration
*/
public function get_configuration_context(): context {
return system::instance();
}
/**
* URL for configuration of the fields on this handler.
*
* @return url The URL to configure custom fields for this component
*/
public function get_configuration_url(): url {
return new url('/reportbuilder/customfield.php');
}
/**
* Returns the context for the data associated with the given instanceid.
*
* @param int $instanceid id of the record to get the context for
* @return context the context for the given record
*/
public function get_instance_context(int $instanceid = 0): context {
if ($instanceid > 0) {
return (manager::get_report_from_id($instanceid))->get_context();
} else {
return system::instance();
}
}
}
@@ -18,13 +18,14 @@ declare(strict_types=1);
namespace core_reportbuilder\external;
use core_user;
use renderer_base;
use core_customfield\external\field_data_exporter;
use core\external\persistent_exporter;
use core\output\renderer_base;
use core_reportbuilder\datasource;
use core_reportbuilder\manager;
use core_reportbuilder\local\models\report;
use core_tag\external\{tag_item_exporter, util};
use core\user;
use core_user\external\user_summary_exporter;
/**
@@ -63,6 +64,7 @@ class custom_report_details_exporter extends persistent_exporter {
'type' => tag_item_exporter::read_properties_definition(),
'multiple' => true,
],
'customfields' => ['type' => field_data_exporter::read_properties_definition()],
'modifiedby' => ['type' => user_summary_exporter::read_properties_definition()],
];
}
@@ -74,12 +76,18 @@ class custom_report_details_exporter extends persistent_exporter {
* @return array
*/
protected function get_other_values(renderer_base $output): array {
$reportid = $this->persistent->get('id');
$source = $this->persistent->get('source');
$usermodified = core_user::get_user($this->persistent->get('usermodified'));
$usermodified = user::get_user($this->persistent->get('usermodified'));
return [
'sourcename' => manager::report_source_exists($source, datasource::class) ? $source::get_name() : null,
'tags' => util::get_item_tags('core_reportbuilder', 'reportbuilder_report', $this->persistent->get('id')),
'tags' => util::get_item_tags('core_reportbuilder', 'reportbuilder_report', $reportid),
'customfields' => (new field_data_exporter(null, [
'component' => 'core_reportbuilder',
'area' => 'report',
'instanceid' => $reportid,
]))->export($output),
'modifiedby' => (new user_summary_exporter($usermodified))->export($output),
];
}
+10 -1
View File
@@ -25,6 +25,7 @@ use moodle_url;
use core_form\dynamic_form;
use core_reportbuilder\datasource;
use core_reportbuilder\manager;
use core_reportbuilder\customfield\report_handler;
use core_reportbuilder\local\helpers\report as reporthelper;
use core_tag_tag;
@@ -92,6 +93,8 @@ class report extends dynamic_form {
public function definition() {
$mform = $this->_form;
$mform->addElement('header', 'general', get_string('general', 'form'));
$mform->addElement('hidden', 'id');
$mform->setType('id', PARAM_INT);
@@ -119,6 +122,10 @@ class report extends dynamic_form {
$mform->addElement('tags', 'tags', get_string('tags'), [
'component' => 'core_reportbuilder', 'itemtype' => 'reportbuilder_report',
]);
// Add custom fields to the form.
$reportid = empty($this->_customdata['id']) ? 0 : $this->_customdata['id'];
report_handler::create()->instance_form_definition($mform, $reportid);
}
/**
@@ -144,7 +151,9 @@ class report extends dynamic_form {
public function set_data_for_dynamic_submission(): void {
if ($persistent = $this->get_custom_report()?->get_report_persistent()) {
$tags = core_tag_tag::get_item_tags_array('core_reportbuilder', 'reportbuilder_report', $persistent->get('id'));
$this->set_data(array_merge((array) $persistent->to_record(), ['tags' => $tags]));
$data = (object) array_merge((array) $persistent->to_record(), ['tags' => $tags]);
report_handler::create()->instance_form_before_set_data($data);
$this->set_data($data);
}
}
@@ -67,6 +67,10 @@ class report {
$report->get_context(), $data->tags);
}
// Report custom fields.
$data->id = $report->get('id');
\core_reportbuilder\customfield\report_handler::create()->instance_form_save($data, true);
return $report;
}
@@ -93,6 +97,9 @@ class report {
$report->get_context(), $data->tags);
}
// Report custom fields.
\core_reportbuilder\customfield\report_handler::create()->instance_form_save($data, false);
return $report;
}
@@ -177,6 +184,12 @@ class report {
}
}
// Duplicate custom fields.
$reportdata = $report->to_record();
\core_reportbuilder\customfield\report_handler::create()->instance_form_before_set_data($reportdata);
$reportdata->id = $newreport->get('id');
\core_reportbuilder\customfield\report_handler::create()->instance_form_save($reportdata);
return $newreport;
}
@@ -28,7 +28,7 @@ use core_reportbuilder\manager;
use core_reportbuilder\system_report;
use core_reportbuilder\local\entities\user;
use core_reportbuilder\local\filters\{boolean_select, date, tags, text, select};
use core_reportbuilder\local\helpers\{audience, format};
use core_reportbuilder\local\helpers\{audience, custom_fields, format};
use core_reportbuilder\local\report\{action, column, filter};
use core_reportbuilder\output\report_name_editable;
use core_reportbuilder\local\models\report;
@@ -288,6 +288,17 @@ class reports_list extends system_report {
$this->add_filter_from_entity('user:userselect')
->set_header(new lang_string('usermodified', 'reportbuilder'))
->set_is_available(has_capability('moodle/user:viewalldetails', $this->get_context()));
// Custom fields filters.
$customfields = new custom_fields(
'rb.id',
$this->get_report_entity_name(),
'core_reportbuilder',
'report',
);
foreach ($customfields->get_filters() as $filter) {
$this->add_filter($filter);
}
}
/**
+38
View File
@@ -0,0 +1,38 @@
<?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/>.
/**
* Manage report custom fields
*
* @package core_reportbuilder
* @copyright 2025 David Carrillo <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once('../config.php');
require_once($CFG->libdir.'/adminlib.php');
admin_externalpage_setup('reportbuildercustomfields');
/** @var \core_customfield\output\renderer $output */
$output = $PAGE->get_renderer('core_customfield');
$handler = core_reportbuilder\customfield\report_handler::create();
$outputpage = new \core_customfield\output\management($handler);
echo $output->header(),
$output->heading(new lang_string('reportbuildercustomfields', 'core_reportbuilder')),
$output->render($outputpage),
$output->footer();
@@ -0,0 +1,43 @@
@core @core_reportbuilder @javascript
Feature: Manage custom fields for custom reports
In order to manage custom fields for custom reports
As an admin
I need to create new and edit existing report custom fields
Scenario: Create and edit custom fields in a custom report
Given the following "core_reportbuilder > Report" exists:
| name | My report |
| source | core_user\reportbuilder\datasource\users |
| default | 1 |
When I log in as "admin"
And I navigate to "Reports > Report builder > Custom report fields" in site administration
Then I should see "Custom report fields"
And I press "Add a new category"
And I should see "Other fields"
And I click on "Add a new custom field" "link"
And I click on "Short text" "link"
And I set the following fields to these values:
| Name | Description |
| Short name | description |
And I press "Save changes"
And I navigate to "Reports > Report builder > Custom reports" in site administration
And I press "Edit report details" action in the "My report" report row
And I should see "Other fields" in the "Edit report details" "dialogue"
And I set the following fields in the "Edit report details" "dialogue" to these values:
| Description | My awesome report description |
And I click on "Save" "button" in the "Edit report details" "dialogue"
And I should see "Report updated"
And I click on "Filters" "button"
And I set the following fields in the "Description" "core_reportbuilder > Filter" to these values:
| Description operator | Contains |
| Description value | awesome |
And I click on "Apply" "button" in the "[data-region='report-filters']" "css_element"
And the following should exist in the "Reports list" table:
| Name | Report source |
| My report | Users |
And I set the following fields in the "Description" "core_reportbuilder > Filter" to these values:
| Description operator | Does not contain |
| Description value | awesome |
And I click on "Apply" "button" in the "[data-region='report-filters']" "css_element"
And I should not see "My report"
And I should see "Nothing to display"
@@ -285,10 +285,17 @@ Feature: Manage custom reports
| username | firstname | lastname | email | suspended |
| user1 | User | 1 | user1@example.com | 1 |
| user2 | User | 2 | user2@example.com | 0 |
And the following "custom field categories" exist:
| name | component | area | itemid |
| Newcat | core_reportbuilder | report | 0 |
And the following "custom fields" exist:
| name | category | type | shortname | description | configdata |
| Myshorttext | Newcat | text | f1 | d1 | |
And the following "core_reportbuilder > Report" exists:
| name | My report |
| source | core_user\reportbuilder\datasource\users |
| default | 1 |
| name | My report |
| source | core_user\reportbuilder\datasource\users |
| default | 1 |
| customfield_f1 | My short text |
And the following "core_reportbuilder > Audience" exists:
| report | My report |
| classname | core_reportbuilder\reportbuilder\audience\allusers |
@@ -317,6 +324,8 @@ Feature: Manage custom reports
And "All users" "core_reportbuilder > Audience" should exist
And I click on the "Schedules" dynamic tab
And I should see "My schedule" in the "Report schedules" "table"
And I press "Edit details"
And the field "Myshorttext" matches value "My short text"
Scenario: Delete custom report
Given the following "core_reportbuilder > Reports" exist:
@@ -19,6 +19,7 @@ declare(strict_types=1);
namespace core_reportbuilder\external;
use advanced_testcase;
use core_customfield_generator;
use core_reportbuilder_generator;
use core_user\reportbuilder\datasource\users;
@@ -39,13 +40,26 @@ final class custom_report_details_exporter_test extends advanced_testcase {
global $PAGE;
$this->resetAfterTest();
$this->setAdminUser();
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
/** @var core_customfield_generator $generator */
$generator = $this->getDataGenerator()->get_plugin_generator('core_customfield');
$category = $generator->create_category(['component' => 'core_reportbuilder', 'area' => 'report']);
$generator->create_field([
'categoryid' => $category->get('id'),
'name' => 'My field',
'shortname' => 'myfield',
'type' => 'number',
]);
/** @var core_reportbuilder_generator $generator */
$generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder');
$report = $generator->create_report(['name' => 'My report', 'source' => users::class, 'tags' => ['cat', 'dog']]);
$report = $generator->create_report([
'name' => 'My report',
'source' => users::class,
'tags' => ['cat', 'dog'],
'customfield_myfield' => 42,
]);
$exporter = new custom_report_details_exporter($report);
$export = $exporter->export($PAGE->get_renderer('core_reportbuilder'));
@@ -62,8 +76,12 @@ final class custom_report_details_exporter_test extends advanced_testcase {
$this->assertObjectHasProperty('tags', $export);
$this->assertEquals(['cat', 'dog'], array_column($export->tags, 'name'));
// We use the custom field exporter for report custom fields.
$this->assertObjectHasProperty('customfields', $export);
$this->assertEquals(['42'], array_column($export->customfields->data, 'value'));
// We use the user exporter for the modifier of the report.
$this->assertObjectHasProperty('modifiedby', $export);
$this->assertEquals(fullname($user), $export->modifiedby->fullname);
$this->assertEquals('Admin User', $export->modifiedby->fullname);
}
}
+23 -2
View File
@@ -19,11 +19,11 @@ declare(strict_types=1);
namespace core_reportbuilder\external\reports;
use context_system;
use core_customfield_generator;
use core_reportbuilder_generator;
use core_external\external_api;
use externallib_advanced_testcase;
use core_reportbuilder\exception\report_access_exception;
use core_reportbuilder\local\models\report;
use core_user\reportbuilder\datasource\users;
defined('MOODLE_INTERNAL') || die();
@@ -48,11 +48,26 @@ final class listing_test extends externallib_advanced_testcase {
$this->resetAfterTest();
$this->setAdminUser();
/** @var core_customfield_generator $generator */
$generator = $this->getDataGenerator()->get_plugin_generator('core_customfield');
$category = $generator->create_category(['component' => 'core_reportbuilder', 'area' => 'report']);
$generator->create_field([
'categoryid' => $category->get('id'),
'name' => 'My field',
'shortname' => 'myfield',
'type' => 'number',
]);
/** @var core_reportbuilder_generator $generator */
$generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder');
// Create three reports.
$reportone = $generator->create_report(['name' => 'Report one', 'source' => users::class, 'tags' => ['cat', 'dog']]);
$reportone = $generator->create_report([
'name' => 'Report one',
'source' => users::class,
'tags' => ['cat', 'dog'],
'customfield_myfield' => 42,
]);
$reporttwo = $generator->create_report(['name' => 'Report two', 'source' => users::class]);
$reportthree = $generator->create_report(['name' => 'Report three', 'source' => users::class]);
@@ -76,6 +91,12 @@ final class listing_test extends externallib_advanced_testcase {
[],
], array_map(fn(array $tags) => array_column($tags, 'name'), $tagscolumn));
$customfieldscolumn = array_column($result['reports'], 'customfields');
$this->assertEquals([
['42'],
[null],
], array_map(fn(array $customfields) => array_column($customfields['data'], 'value'), $customfieldscolumn));
$this->assertEmpty($result['warnings']);
}
+19 -2
View File
@@ -18,6 +18,7 @@ declare(strict_types=1);
namespace core_reportbuilder\external\reports;
use core_customfield_generator;
use core_reportbuilder_generator;
use core_external\external_api;
use externallib_advanced_testcase;
@@ -49,11 +50,26 @@ final class retrieve_test extends externallib_advanced_testcase {
$this->getDataGenerator()->create_user(['firstname' => 'Zoe', 'lastname' => 'Zebra', 'email' => '[email protected]']);
$this->getDataGenerator()->create_user(['firstname' => 'Charlie', 'lastname' => 'Carrot', 'email' => '[email protected]']);
/** @var core_customfield_generator $generator */
$generator = $this->getDataGenerator()->get_plugin_generator('core_customfield');
$category = $generator->create_category(['component' => 'core_reportbuilder', 'area' => 'report']);
$generator->create_field([
'categoryid' => $category->get('id'),
'name' => 'My field',
'shortname' => 'myfield',
'type' => 'number',
]);
/** @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,
'tags' => ['cat', 'dog']]);
$report = $generator->create_report([
'name' => 'My report',
'source' => users::class,
'default' => false,
'tags' => ['cat', 'dog'],
'customfield_myfield' => 42,
]);
$generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:fullname', 'sortenabled' => 1]);
$generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:email']);
@@ -87,6 +103,7 @@ final class retrieve_test extends externallib_advanced_testcase {
$this->assertArrayHasKey('details', $result);
$this->assertEquals('My report', $result['details']['name']);
$this->assertEquals(['cat', 'dog'], array_column($result['details']['tags'], 'name'));
$this->assertEquals(['42'], array_column($result['details']['customfields']['data'], 'value'));
$this->assertArrayHasKey('data', $result);
$this->assertEquals(['Full name', 'Email address'], $result['data']['headers']);
+3
View File
@@ -61,6 +61,9 @@ class core_reportbuilder_generator extends component_generator_base {
// Include default setup unless specifically disabled in passed record.
$default = (bool) ($record['default'] ?? true);
// Report custom fields.
\core_reportbuilder\customfield\report_handler::create()->instance_form_before_set_data((object)$record);
// If setting up default report, purge caches to ensure any default attributes are always loaded in tests.
$report = helper::create_report((object) $record, $default);
if ($default) {
+1 -1
View File
@@ -29,7 +29,7 @@
defined('MOODLE_INTERNAL') || die();
$version = 2025030800.00; // YYYYMMDD = weekly release date of this DEV branch.
$version = 2025030800.01; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.
$release = '5.0dev+ (Build: 20250308)'; // Human-friendly version name