This commit is contained in:
Sara Arjona 2024-03-21 15:01:18 +01:00
commit 484e52f6ae
No known key found for this signature in database
17 changed files with 493 additions and 44 deletions

View File

@ -81,6 +81,7 @@ $string['courseidnumberewithlink'] = 'Course ID number with link';
$string['courseshortnamewithlink'] = 'Course short name with link';
$string['courseselect'] = 'Select course';
$string['customfieldcolumn'] = '{$a}';
$string['customreport'] = 'Custom report';
$string['customreports'] = 'Custom reports';
$string['customreportslimit'] = 'Custom reports limit';
$string['customreportslimit_desc'] = 'The number of custom reports may be limited for performance reasons. If set to zero, then there is no limit.';
@ -263,6 +264,7 @@ $string['sorting'] = 'Sorting';
$string['sorting_help'] = 'You can set the initial sort order of columns in the report, which can then be changed by users by clicking on column names.';
$string['switchedit'] = 'Switch to edit mode';
$string['switchpreview'] = 'Switch to preview mode';
$string['tagarea_reportbuilder_report'] = 'Custom reports';
$string['tasksendschedule'] = 'Send report schedule';
$string['tasksendschedules'] = 'Send report schedules';
$string['timeadded'] = 'Time added';

View File

@ -93,4 +93,10 @@ $tagareas = [
'callback' => 'badge_get_tagged_badges',
'callbackfile' => '/badges/lib.php',
],
[
'itemtype' => 'reportbuilder_report',
'component' => 'core_reportbuilder',
'callback' => 'core_reportbuilder_get_tagged_reports',
'callbackfile' => '/reportbuilder/lib.php',
],
];

View File

@ -26,6 +26,7 @@ use core_form\dynamic_form;
use core_reportbuilder\datasource;
use core_reportbuilder\manager;
use core_reportbuilder\local\helpers\report as reporthelper;
use core_tag_tag;
defined('MOODLE_INTERNAL') || die();
@ -114,6 +115,10 @@ class report extends dynamic_form {
$mform->addElement('advcheckbox', 'uniquerows', get_string('uniquerows', 'core_reportbuilder'));
$mform->addHelpButton('uniquerows', 'uniquerows', 'core_reportbuilder');
$mform->addElement('tags', 'tags', get_string('tags'), [
'component' => 'core_reportbuilder', 'itemtype' => 'reportbuilder_report',
]);
}
/**
@ -137,8 +142,9 @@ class report extends dynamic_form {
* Load in existing data as form defaults
*/
public function set_data_for_dynamic_submission(): void {
if ($report = $this->get_custom_report()) {
$this->set_data($report->get_report_persistent()->to_record());
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]));
}
}

View File

@ -18,17 +18,24 @@ declare(strict_types=1);
namespace core_reportbuilder\local\filters;
use coding_exception;
use core_tag_tag;
use lang_string;
use MoodleQuickForm;
use stdClass;
use core_reportbuilder\local\helpers\database;
use core_reportbuilder\local\report\filter;
/**
* Class containing logic for the tags filter
*
* The field SQL should be the field containing the ID of the {tag} table
* The filter can operate in two modes:
*
* 1. Filtering of tags directly from the {tag} table, in which case the field SQL expression should return the ID of that table;
* 2. Filtering of component tags, in which case the field SQL expression should return the ID of the component table that would
* join to the {tag_instance} itemid field
*
* If filtering component tags then the following must be passed to the {@see filter::get_options} method when using this filter
* in a report: ['component' => 'mycomponent', 'itemtype' => 'myitem']
*
* @package core_reportbuilder
* @copyright 2022 Paul Holden <paulh@moodle.com>
@ -80,14 +87,26 @@ class tags extends base {
$mform->addElement('select', "{$this->name}_operator", $operatorlabel, $this->get_operators())
->setHiddenLabel(true);
$sql = 'SELECT DISTINCT t.id, t.name, t.rawname
// If we're filtering component tags, show only those related to the component itself.
$options = (array) $this->filter->get_options();
if (array_key_exists('component', $options) && array_key_exists('itemtype', $options)) {
$taginstancejoin = 'JOIN {tag_instance} ti ON ti.tagid = t.id
WHERE ti.component = :component AND ti.itemtype = :itemtype';
$params = array_intersect_key($options, array_flip(['component', 'itemtype']));
} else {
$taginstancejoin = '';
$params = [];
}
$sql = "SELECT DISTINCT t.id, t.name, t.rawname
FROM {tag} t
ORDER BY t.name';
{$taginstancejoin}
ORDER BY t.name";
// Transform tag records into appropriate display name, for selection in the autocomplete element.
$tags = array_map(static function(stdClass $record): string {
return core_tag_tag::make_display_name($record);
}, $DB->get_records_sql($sql));
}, $DB->get_records_sql($sql, $params));
$valuelabel = get_string('filterfieldvalue', 'core_reportbuilder', $this->get_header());
$mform->addElement('autocomplete', "{$this->name}_value", $valuelabel, $tags, ['multiple' => true])
@ -110,26 +129,66 @@ class tags extends base {
$operator = (int) ($values["{$this->name}_operator"] ?? self::ANY_VALUE);
$tags = (array) ($values["{$this->name}_value"] ?? []);
if ($operator === self::NOT_EMPTY) {
$select = "{$fieldsql} IS NOT NULL";
} else if ($operator === self::EMPTY) {
$select = "{$fieldsql} IS NULL";
} else if ($operator === self::EQUAL_TO && !empty($tags)) {
[$tagselect, $tagselectparams] = $DB->get_in_or_equal($tags, SQL_PARAMS_NAMED,
database::generate_param_name('_'));
// If we're filtering component tags, we need to perform [not] exists queries to ensure no row duplication occurs.
$options = (array) $this->filter->get_options();
if (array_key_exists('component', $options) && array_key_exists('itemtype', $options)) {
[$paramcomponent, $paramitemtype] = database::generate_param_names(2);
$select = "{$fieldsql} {$tagselect}";
$params = array_merge($params, $tagselectparams);
} else if ($operator === self::NOT_EQUAL_TO && !empty($tags)) {
[$tagselect, $tagselectparams] = $DB->get_in_or_equal($tags, SQL_PARAMS_NAMED,
database::generate_param_name('_'), false);
$componenttagselect = <<<EOF
SELECT 1
FROM {tag} t
JOIN {tag_instance} ti ON ti.tagid = t.id
WHERE ti.component = :{$paramcomponent} AND ti.itemtype = :{$paramitemtype} AND ti.itemid = {$fieldsql}
EOF;
// We should also return those elements that aren't tagged at all.
$select = "COALESCE({$fieldsql}, 0) {$tagselect}";
$params = array_merge($params, $tagselectparams);
$params[$paramcomponent] = $options['component'];
$params[$paramitemtype] = $options['itemtype'];
if ($operator === self::NOT_EMPTY) {
$select = "EXISTS ({$componenttagselect})";
} else if ($operator === self::EMPTY) {
$select = "NOT EXISTS ({$componenttagselect})";
} else if ($operator === self::EQUAL_TO && !empty($tags)) {
[$tagselect, $tagselectparams] = $DB->get_in_or_equal($tags, SQL_PARAMS_NAMED,
database::generate_param_name('_'));
$select = "EXISTS ({$componenttagselect} AND t.id {$tagselect})";
$params = array_merge($params, $tagselectparams);
} else if ($operator === self::NOT_EQUAL_TO && !empty($tags)) {
[$tagselect, $tagselectparams] = $DB->get_in_or_equal($tags, SQL_PARAMS_NAMED,
database::generate_param_name('_'));
// We should also return those elements that aren't tagged at all.
$select = "NOT EXISTS ({$componenttagselect} AND t.id {$tagselect})";
$params = array_merge($params, $tagselectparams);
} else {
// Invalid/inactive (any value) filter..
return ['', []];
}
} else {
// Invalid/inactive (any value) filter..
return ['', []];
// We're filtering directly from the tag table.
if ($operator === self::NOT_EMPTY) {
$select = "{$fieldsql} IS NOT NULL";
} else if ($operator === self::EMPTY) {
$select = "{$fieldsql} IS NULL";
} else if ($operator === self::EQUAL_TO && !empty($tags)) {
[$tagselect, $tagselectparams] = $DB->get_in_or_equal($tags, SQL_PARAMS_NAMED,
database::generate_param_name('_'));
$select = "{$fieldsql} {$tagselect}";
$params = array_merge($params, $tagselectparams);
} else if ($operator === self::NOT_EQUAL_TO && !empty($tags)) {
[$tagselect, $tagselectparams] = $DB->get_in_or_equal($tags, SQL_PARAMS_NAMED,
database::generate_param_name('_'), false);
// We should also return those elements that aren't tagged at all.
$select = "COALESCE({$fieldsql}, 0) {$tagselect}";
$params = array_merge($params, $tagselectparams);
} else {
// Invalid/inactive (any value) filter..
return ['', []];
}
}
return [$select, $params];

View File

@ -26,6 +26,7 @@ use core_reportbuilder\manager;
use core_reportbuilder\local\models\column;
use core_reportbuilder\local\models\filter;
use core_reportbuilder\local\models\report as report_model;
use core_tag_tag;
/**
* Helper class for manipulating custom reports and their elements (columns, filters, conditions, etc)
@ -48,19 +49,24 @@ class report {
$data->name = trim($data->name);
$data->type = datasource::TYPE_CUSTOM_REPORT;
$reportpersistent = manager::create_report_persistent($data);
// Create report persistent.
$report = manager::create_report_persistent($data);
// Add datasource default columns, filters and conditions to the report.
if ($default) {
$source = $reportpersistent->get('source');
$source = $report->get('source');
/** @var datasource $datasource */
$datasource = new $source($reportpersistent, []);
$datasource = new $source($report);
$datasource->add_default_columns();
$datasource->add_default_filters();
$datasource->add_default_conditions();
}
return $reportpersistent;
// Report tags.
core_tag_tag::set_item_tags('core_reportbuilder', 'reportbuilder_report', $report->get('id'),
$report->get_context(), $data->tags);
return $report;
}
/**
@ -80,6 +86,10 @@ class report {
'uniquerows' => $data->uniquerows,
])->update();
// Report tags.
core_tag_tag::set_item_tags('core_reportbuilder', 'reportbuilder_report', $report->get('id'),
$report->get_context(), $data->tags);
return $report;
}
@ -96,6 +106,9 @@ class report {
throw new invalid_parameter_exception('Invalid report');
}
// Report tags.
core_tag_tag::remove_all_item_tags('core_reportbuilder', 'reportbuilder_report', $report->get('id'));
return $report->delete();
}

View File

@ -28,6 +28,7 @@ use core_reportbuilder\manager;
use core_reportbuilder\system_report;
use core_reportbuilder\local\entities\user;
use core_reportbuilder\local\filters\date;
use core_reportbuilder\local\filters\tags;
use core_reportbuilder\local\filters\text;
use core_reportbuilder\local\filters\select;
use core_reportbuilder\local\helpers\audience;
@ -38,6 +39,7 @@ use core_reportbuilder\local\report\filter;
use core_reportbuilder\output\report_name_editable;
use core_reportbuilder\local\models\report;
use core_reportbuilder\permission;
use core_tag_tag;
/**
* Reports list
@ -113,6 +115,8 @@ class reports_list extends system_report {
* Add columns to report
*/
protected function add_columns(): void {
global $DB;
$tablealias = $this->get_main_table_alias();
// Report name column.
@ -158,6 +162,37 @@ class reports_list extends system_report {
})
);
// Tags column. TODO: Reuse tag entity column when MDL-76392 is integrated.
$tagfieldconcatsql = $DB->sql_group_concat(
field: $DB->sql_concat_join("'|'", ['t.name', 't.rawname']),
sort: 't.name',
);
$this->add_column((new column(
'tags',
new lang_string('tags'),
$this->get_report_entity_name(),
))
->set_type(column::TYPE_TEXT)
->add_field("(
SELECT {$tagfieldconcatsql}
FROM {tag_instance} ti
JOIN {tag} t ON t.id = ti.tagid
WHERE ti.component = 'core_reportbuilder' AND ti.itemtype = 'reportbuilder_report'
AND ti.itemid = {$tablealias}.id
)", 'tags')
->set_is_sortable(true)
->set_is_available(core_tag_tag::is_enabled('core_reportbuilder', 'reportbuilder_report'))
->add_callback(static function(?string $tags): string {
return implode(', ', array_map(static function(string $tag): string {
[$name, $rawname] = explode('|', $tag);
return core_tag_tag::make_display_name((object) [
'name' => $name,
'rawname' => $rawname,
]);
}, preg_split('/, /', (string) $tags, -1, PREG_SPLIT_NO_EMPTY)));
})
);
// Time created column.
$this->add_column((new column(
'timecreated',
@ -218,6 +253,21 @@ class reports_list extends system_report {
})
);
// Tags filter.
$this->add_filter((new filter(
tags::class,
'tags',
new lang_string('tags'),
$this->get_report_entity_name(),
"{$tablealias}.id",
))
->set_options([
'component' => 'core_reportbuilder',
'itemtype' => 'reportbuilder_report',
])
->set_is_available(core_tag_tag::is_enabled('core_reportbuilder', 'reportbuilder_report'))
);
// Time created filter.
$this->add_filter((new filter(
date::class,

View File

@ -27,6 +27,9 @@ declare(strict_types=1);
use core\output\inplace_editable;
use core_reportbuilder\form\audience;
use core_reportbuilder\form\filter;
use core_reportbuilder\local\helpers\audience as audience_helper;
use core_reportbuilder\local\models\report;
use core_tag\output\{tagfeed, tagindex};
/**
* Return the filters form fragment
@ -74,6 +77,60 @@ function core_reportbuilder_output_fragment_audience_form(array $params): string
return $renderer->render_from_template('core_reportbuilder/local/audience/form', $context);
}
/**
* Callback to return tagged reports
*
* @param core_tag_tag $tag
* @param bool $exclusivemode
* @param int|null $fromcontextid
* @param int|null $contextid
* @param bool $recurse
* @param int $page
* @return tagindex
*/
function core_reportbuilder_get_tagged_reports(
core_tag_tag $tag,
bool $exclusivemode = false,
?int $fromcontextid = 0,
?int $contextid = 0,
bool $recurse = true,
int $page = 0,
): tagindex {
global $OUTPUT;
// Limit the returned list to those reports the current user can access.
[$where, $params] = audience_helper::user_reports_list_access_sql('it');
$tagcount = $tag->count_tagged_items('core_reportbuilder', 'reportbuilder_report', $where, $params);
$perpage = $exclusivemode ? 20 : 5;
$pagecount = ceil($tagcount / $perpage);
$content = '';
if ($tagcount > 0) {
$tagfeed = new tagfeed();
$pixicon = new pix_icon('i/report', new lang_string('customreport', 'core_reportbuilder'));
$reports = $tag->get_tagged_items('core_reportbuilder', 'reportbuilder_report', $page * $perpage, $perpage,
$where, $params);
foreach ($reports as $report) {
$tagfeed->add(
$OUTPUT->render($pixicon),
html_writer::link(
new moodle_url('/reportbuilder/view.php', ['id' => $report->id]),
(new report(0, $report))->get_formatted_name(),
),
);
}
$content = $OUTPUT->render_from_template('core_tag/tagfeed', $tagfeed->export_for_template($OUTPUT));
}
return new tagindex($tag, 'core_reportbuilder', 'reportbuilder_report', $content, $exclusivemode, $fromcontextid,
$contextid, $recurse, $page, $pagecount);
}
/**
* Plugin inplace editable implementation
*

View File

@ -92,8 +92,12 @@ Feature: Manage custom reports
And I set the following fields in the "New report" "dialogue" to these values:
| Name | Manager report |
| Report source | Users |
| Tags | Cat, Dog |
And I click on "Save" "button" in the "New report" "dialogue"
And I click on "Close 'Manager report' editor" "button"
And the following should exist in the "Reports list" table:
| Name | Tags | Report source |
| Manager report | Cat, Dog | Users |
# Manager can edit their own report, but not those of other users.
And I set the field "Edit report name" in the "Manager report" "table_row" to "Manager report (renamed)"
Then the "Edit report content" item should exist in the "Actions" action menu of the "Manager report (renamed)" "table_row"
@ -140,16 +144,18 @@ Feature: Manage custom reports
When I press "Edit report details" action in the "My report" report row
And I set the following fields in the "Edit report details" "dialogue" to these values:
| Name | My renamed report |
| Tags | Cat, Dog |
And I click on "Save" "button" in the "Edit report details" "dialogue"
Then I should see "Report updated"
And the following should exist in the "Reports list" table:
| Name | Report source |
| My renamed report | Users |
| Name | Tags | Report source |
| My renamed report | Cat, Dog | Users |
Scenario Outline: Filter custom reports
Given the following "core_reportbuilder > Reports" exist:
| name | source |
| My users | core_user\reportbuilder\datasource\users |
| name | source | tags |
| My users | core_user\reportbuilder\datasource\users | Cat, Dog |
| My courses | core_course\reportbuilder\datasource\courses | |
And I log in as "admin"
When I navigate to "Reports > Report builder > Custom reports" in site administration
And I click on "Filters" "button"
@ -158,11 +164,30 @@ Feature: Manage custom reports
| <filter> value | <value> |
And I click on "Apply" "button" in the "[data-region='report-filters']" "css_element"
Then I should see "Filters applied"
And I should see "My users" in the "Reports list" "table"
And the following should exist in the "Reports list" table:
| Name | Tags | Report source |
| My users | Cat, Dog | Users |
And I should not see "My courses" in the "Reports list" "table"
Examples:
| filter | value |
| Name | My users |
| Report source | Users |
| Tags | Cat |
Scenario: Custom report tags are not displayed if tagging is disabled
Given the following config values are set as admin:
| usetags | 0 |
And the following "core_reportbuilder > Reports" exist:
| name | source |
| My report | core_user\reportbuilder\datasource\users |
And I log in as "admin"
When I navigate to "Reports > Report builder > Custom reports" in site administration
Then the following should exist in the "Reports list" table:
| Name | Report source |
| My report | Users |
And "Tags" "link" should not exist in the "Reports list" "table"
And I click on "Filters" "button"
And "Tags" "core_reportbuilder > Filter" should not exist
Scenario: Delete custom report
Given the following "core_reportbuilder > Reports" exist:

View File

@ -50,20 +50,28 @@ class system_report_data_exporter_test extends advanced_testcase {
// Two reports, created one second apart to ensure consistent ordering by time created.
$generator->create_report(['name' => 'My first report', 'source' => users::class]);
$this->waitForSecond();
$generator->create_report(['name' => 'My second report', 'source' => users::class]);
$generator->create_report(['name' => 'My second report', 'source' => users::class, 'tags' => ['cat', 'dog']]);
$reportinstance = system_report_factory::create(reports_list::class, system::instance());
$exporter = new system_report_data_exporter(null, ['report' => $reportinstance, 'page' => 0, 'perpage' => 1]);
$export = $exporter->export($PAGE->get_renderer('core_reportbuilder'));
$this->assertEquals(['Name', 'Report source', 'Time created', 'Time modified', 'Modified by'], $export->headers);
$this->assertEquals([
'Name',
'Report source',
'Tags',
'Time created',
'Time modified',
'Modified by',
], $export->headers);
$this->assertCount(1, $export->rows);
[$name, $source, $timecreated, $timemodified, $modifiedby] = $export->rows[0]['columns'];
[$name, $source, $tags, $timecreated, $timemodified, $modifiedby] = $export->rows[0]['columns'];
$this->assertStringContainsString('My second report', $name);
$this->assertEquals(users::get_name(), $source);
$this->assertEquals('cat, dog', $tags);
$this->assertNotEmpty($timecreated);
$this->assertNotEmpty($timemodified);
$this->assertEquals('Admin User', $modifiedby);

View File

@ -54,20 +54,28 @@ class retrieve_test extends externallib_advanced_testcase {
// Two reports, created one second apart to ensure consistent ordering by time created.
$generator->create_report(['name' => 'My first report', 'source' => users::class]);
$this->waitForSecond();
$generator->create_report(['name' => 'My second report', 'source' => users::class]);
$generator->create_report(['name' => 'My second report', 'source' => users::class, 'tags' => ['cat', 'dog']]);
// Retrieve paged results.
$result = retrieve::execute(reports_list::class, ['contextid' => system::instance()->id], '', '', 0, [], 0, 1);
$result = external_api::clean_returnvalue(retrieve::execute_returns(), $result);
$this->assertArrayHasKey('data', $result);
$this->assertEquals(['Name', 'Report source', 'Time created', 'Time modified', 'Modified by'], $result['data']['headers']);
$this->assertEquals([
'Name',
'Report source',
'Tags',
'Time created',
'Time modified',
'Modified by',
], $result['data']['headers']);
$this->assertCount(1, $result['data']['rows']);
[$name, $source, $timecreated, $timemodified, $modifiedby] = $result['data']['rows'][0]['columns'];
[$name, $source, $tags, $timecreated, $timemodified, $modifiedby] = $result['data']['rows'][0]['columns'];
$this->assertStringContainsString('My second report', $name);
$this->assertEquals(users::get_name(), $source);
$this->assertEquals('cat, dog', $tags);
$this->assertNotEmpty($timecreated);
$this->assertNotEmpty($timemodified);
$this->assertEquals('Admin User', $modifiedby);

View File

@ -51,6 +51,12 @@ class core_reportbuilder_generator extends component_generator_base {
throw new coding_exception('Record must contain \'source\' property');
}
// Report tags.
$tags = $record['tags'] ?? '';
if (!is_array($tags)) {
$record['tags'] = preg_split('/\s*,\s*/', $tags, -1, PREG_SPLIT_NO_EMPTY);
}
// Include default setup unless specifically disabled in passed record.
$default = (bool) ($record['default'] ?? true);

View File

@ -21,6 +21,7 @@ namespace core_reportbuilder;
use advanced_testcase;
use core_reportbuilder_generator;
use core_reportbuilder\local\models\{audience, column, filter, report, schedule};
use core_tag_tag;
use core_user\reportbuilder\datasource\users;
/**
@ -44,9 +45,11 @@ class generator_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]);
$report = $generator->create_report(['name' => 'My report', 'source' => users::class, 'tags' => ['cat', 'dog']]);
$this->assertTrue(report::record_exists($report->get('id')));
$this->assertEqualsCanonicalizing(['cat', 'dog'],
core_tag_tag::get_item_tags_array('core_reportbuilder', 'reportbuilder_report', $report->get('id')));
}
/**

View File

@ -0,0 +1,78 @@
<?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;
use advanced_testcase;
use core_reportbuilder_generator;
use core_tag_tag;
use core_user\reportbuilder\datasource\users;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once("{$CFG->dirroot}/reportbuilder/lib.php");
/**
* Unit tests for the component callbacks
*
* @package core_reportbuilder
* @copyright 2023 Paul Holden <paulh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class lib_test extends advanced_testcase {
/**
* Test getting tagged reports
*
* @covers ::core_reportbuilder_get_tagged_reports
*/
public function test_core_reportbuilder_get_tagged_reports(): void {
$this->resetAfterTest();
/** @var core_reportbuilder_generator $generator */
$generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder');
// Create three tagged reports.
$reportone = $generator->create_report(['name' => 'Report 1', 'source' => users::class, 'tags' => ['cat']]);
$reporttwo = $generator->create_report(['name' => 'Report 2', 'source' => users::class, 'tags' => ['dog']]);
$reportthree = $generator->create_report(['name' => 'Report 3', 'source' => users::class, 'tags' => ['cat']]);
// Add all users audience to report one and two.
$generator->create_audience(['reportid' => $reportone->get('id'), 'configdata' => []]);
$generator->create_audience(['reportid' => $reporttwo->get('id'), 'configdata' => []]);
$tag = core_tag_tag::get_by_name(0, 'cat');
// Current user can only access report one with "cat" tag.
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
$tagindex = core_reportbuilder_get_tagged_reports($tag);
$this->assertStringContainsString($reportone->get_formatted_name(), $tagindex->content);
$this->assertStringNotContainsString($reporttwo->get_formatted_name(), $tagindex->content);
$this->assertStringNotContainsString($reportthree->get_formatted_name(), $tagindex->content);
// Admin can access both reports with "cat" tag.
$this->setAdminUser();
$tagindex = core_reportbuilder_get_tagged_reports($tag);
$this->assertStringContainsString($reportone->get_formatted_name(), $tagindex->content);
$this->assertStringNotContainsString($reporttwo->get_formatted_name(), $tagindex->content);
$this->assertStringContainsString($reportthree->get_formatted_name(), $tagindex->content);
}
}

View File

@ -20,7 +20,9 @@ namespace core_reportbuilder\local\filters;
use advanced_testcase;
use lang_string;
use core_reportbuilder_generator;
use core_reportbuilder\local\report\filter;
use core_user\reportbuilder\datasource\users;
/**
* Unit tests for tags report filter
@ -38,7 +40,7 @@ class tags_test extends advanced_testcase {
*
* @return array[]
*/
public function get_sql_filter_provider(): array {
public static function get_sql_filter_provider(): array {
return [
'Any value' => [tags::ANY_VALUE, null, ['course01', 'course01', 'course02', 'course03']],
'Not empty' => [tags::NOT_EMPTY, null, ['course01', 'course01', 'course02']],
@ -102,4 +104,78 @@ class tags_test extends advanced_testcase {
$courses = $DB->get_fieldset_sql($sql, $params);
$this->assertEqualsCanonicalizing($expectedcoursenames, $courses);
}
/**
* Data provider for {@see test_get_sql_filter_component}
*
* @return array[]
*/
public static function get_sql_filter_component_provider(): array {
return [
'Any value' => [tags::ANY_VALUE, null, ['report01', 'report02']],
'Not empty' => [tags::NOT_EMPTY, null, ['report01']],
'Empty' => [tags::EMPTY, null, ['report02']],
'Equal to unselected' => [tags::EQUAL_TO, null, ['report01', 'report02']],
'Equal to selected tag' => [tags::EQUAL_TO, 'fish', ['report01']],
'Equal to selected tag (different component)' => [tags::EQUAL_TO, 'cat', []],
'Not equal to unselected' => [tags::NOT_EQUAL_TO, null, ['report01', 'report02']],
'Not equal to selected tag' => [tags::NOT_EQUAL_TO, 'fish', ['report02']],
'Not Equal to selected tag (different component)' => [tags::NOT_EQUAL_TO, 'cat', ['report01', 'report02']],
];
}
/**
* Test getting filter SQL
*
* @param int $operator
* @param string|null $tagname
* @param array $expectedreportnames
*
* @dataProvider get_sql_filter_component_provider
*/
public function test_get_sql_filter_component(int $operator, ?string $tagname, array $expectedreportnames): void {
global $DB;
$this->resetAfterTest();
// Create a course with tags, we shouldn't ever get this data back when specifying another component.
$this->getDataGenerator()->create_course(['tags' => ['cat', 'dog']]);
/** @var core_reportbuilder_generator $generator */
$generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder');
$generator->create_report(['name' => 'report01', 'source' => users::class, 'tags' => ['fish']]);
$generator->create_report(['name' => 'report02', 'source' => users::class]);
$filter = (new filter(
tags::class,
'tags',
new lang_string('tags'),
'testentity',
'r.id'
))->set_options([
'component' => 'core_reportbuilder',
'itemtype' => 'reportbuilder_report',
]);
// Create instance of our filter, passing ID of the tag if specified.
if ($tagname !== null) {
$tagid = $DB->get_field('tag', 'id', ['name' => $tagname], MUST_EXIST);
$value = [$tagid];
} else {
$value = null;
}
[$select, $params] = tags::create($filter)->get_sql_filter([
$filter->get_unique_identifier() . '_operator' => $operator,
$filter->get_unique_identifier() . '_value' => $value,
]);
$sql = 'SELECT r.name FROM {reportbuilder_report} r';
if ($select) {
$sql .= " WHERE {$select}";
}
$reports = $DB->get_fieldset_sql($sql, $params);
$this->assertEqualsCanonicalizing($expectedreportnames, $reports);
}
}

View File

@ -21,8 +21,10 @@ namespace core_reportbuilder\local\helpers;
use advanced_testcase;
use core_reportbuilder_generator;
use invalid_parameter_exception;
use core_reportbuilder\datasource;
use core_reportbuilder\local\models\column;
use core_reportbuilder\local\models\filter;
use core_tag_tag;
use core_user\reportbuilder\datasource\users;
/**
@ -35,6 +37,49 @@ use core_user\reportbuilder\datasource\users;
*/
class report_test extends advanced_testcase {
/**
* Test creation report
*/
public function test_create_report(): void {
$this->resetAfterTest();
$this->setAdminUser();
$report = report::create_report((object) [
'name' => 'My report',
'source' => users::class,
'tags' => ['cat', 'dog'],
]);
$this->assertEquals('My report', $report->get('name'));
$this->assertEquals(datasource::TYPE_CUSTOM_REPORT, $report->get('type'));
$this->assertEqualsCanonicalizing(['cat', 'dog'],
core_tag_tag::get_item_tags_array('core_reportbuilder', 'reportbuilder_report', $report->get('id')));
}
/**
* Test updating report
*/
public function test_update_report(): void {
$this->resetAfterTest();
$this->setAdminUser();
/** @var core_reportbuilder_generator $generator */
$generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder');
$report = $generator->create_report(['name' => 'My report', 'source' => users::class, 'uniquerows' => 0]);
$reportupdated = report::update_report((object) [
'id' => $report->get('id'),
'name' => 'My renamed report',
'uniquerows' => 1,
'tags' => ['cat', 'dog'],
]);
$this->assertEquals('My renamed report', $reportupdated->get('name'));
$this->assertTrue($reportupdated->get('uniquerows'));
$this->assertEqualsCanonicalizing(['cat', 'dog'],
core_tag_tag::get_item_tags_array('core_reportbuilder', 'reportbuilder_report', $reportupdated->get('id')));
}
/**
* Test deleting report
*/
@ -46,7 +91,8 @@ class report_test extends advanced_testcase {
$generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder');
// Create Report1 and add some elements.
$report1 = $generator->create_report(['name' => 'My report 1', 'source' => users::class, 'default' => false]);
$report1 = $generator->create_report(['name' => 'My report 1', 'source' => users::class, 'default' => false,
'tags' => ['cat', 'dog']]);
$column1 = $generator->create_column(['reportid' => $report1->get('id'), 'uniqueidentifier' => 'user:email']);
$filter1 = $generator->create_filter(['reportid' => $report1->get('id'), 'uniqueidentifier' => 'user:email']);
$condition1 = $generator->create_condition(['reportid' => $report1->get('id'), 'uniqueidentifier' => 'user:email']);
@ -58,13 +104,15 @@ class report_test extends advanced_testcase {
$condition2 = $generator->create_condition(['reportid' => $report2->get('id'), 'uniqueidentifier' => 'user:email']);
// Delete Report1.
report::delete_report($report1->get('id'));
$result = report::delete_report($report1->get('id'));
$this->assertTrue($result);
// Make sure Report1, and all it's elements are deleted.
$this->assertFalse($report1::record_exists($report1->get('id')));
$this->assertFalse($column1::record_exists($column1->get('id')));
$this->assertFalse($filter1::record_exists($filter1->get('id')));
$this->assertFalse($condition1::record_exists($condition1->get('id')));
$this->assertEmpty(core_tag_tag::get_item_tags_array('core_reportbuilder', 'reportbuilder_report', $report1->get('id')));
// Make sure Report2, and all it's elements still exist.
$this->assertTrue($report2::record_exists($report2->get('id')));

View File

@ -15,6 +15,10 @@ Information provided here is intended especially for developers.
* The base datasource `add_all_from_entity` method accepts additional parameters to limit which columns, filters and conditions
are added. The `add_[columns|filters|conditions]_from_entity` class methods also now support wildcard matching in both `$include`
and `$exclude` parameters
* Custom reports now implement the tag API, with options for specifying in the `report::[create|update]_report` helper methods
as well as in the `create_report` test generator method
* The `tags` filter has been improved to also allow for filtering by component/itemtype core_tag definition - this is more
suited for system reports
* New report filter types:
- `cohort` for reports containing cohort data
- `courserole` for reports showing course enrolments

View File

@ -29,7 +29,7 @@
defined('MOODLE_INTERNAL') || die();
$version = 2024032000.00; // YYYYMMDD = weekly release date of this DEV branch.
$version = 2024032000.01; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.
$release = '4.4dev+ (Build: 20240320)'; // Human-friendly version name