mirror of
https://github.com/moodle/moodle.git
synced 2025-03-14 20:50:21 +01:00
Merge branch 'MDL-80245' of https://github.com/paulholden/moodle
This commit is contained in:
commit
484e52f6ae
@ -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';
|
||||
|
@ -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',
|
||||
],
|
||||
];
|
||||
|
@ -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]));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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];
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
*
|
||||
|
@ -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:
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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')));
|
||||
}
|
||||
|
||||
/**
|
||||
|
78
reportbuilder/tests/lib_test.php
Normal file
78
reportbuilder/tests/lib_test.php
Normal 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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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')));
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user