diff --git a/lang/en/reportbuilder.php b/lang/en/reportbuilder.php index ae2cd8ac617..2ba2d394d87 100644 --- a/lang/en/reportbuilder.php +++ b/lang/en/reportbuilder.php @@ -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'; diff --git a/lib/db/tag.php b/lib/db/tag.php index 22e125ecbba..834e53c2f84 100644 --- a/lib/db/tag.php +++ b/lib/db/tag.php @@ -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', + ], ]; diff --git a/reportbuilder/classes/form/report.php b/reportbuilder/classes/form/report.php index 2bf6c569db5..8d70cea0389 100644 --- a/reportbuilder/classes/form/report.php +++ b/reportbuilder/classes/form/report.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])); } } diff --git a/reportbuilder/classes/local/filters/tags.php b/reportbuilder/classes/local/filters/tags.php index a0730d24c2c..b04de1d305e 100644 --- a/reportbuilder/classes/local/filters/tags.php +++ b/reportbuilder/classes/local/filters/tags.php @@ -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 @@ -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 = <<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]; diff --git a/reportbuilder/classes/local/helpers/report.php b/reportbuilder/classes/local/helpers/report.php index 4e69f704861..4471e0b1163 100644 --- a/reportbuilder/classes/local/helpers/report.php +++ b/reportbuilder/classes/local/helpers/report.php @@ -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(); } diff --git a/reportbuilder/classes/local/systemreports/reports_list.php b/reportbuilder/classes/local/systemreports/reports_list.php index b588a89943d..ba726e71801 100644 --- a/reportbuilder/classes/local/systemreports/reports_list.php +++ b/reportbuilder/classes/local/systemreports/reports_list.php @@ -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, diff --git a/reportbuilder/lib.php b/reportbuilder/lib.php index 4a0f575c1b4..08c888b0195 100644 --- a/reportbuilder/lib.php +++ b/reportbuilder/lib.php @@ -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 * diff --git a/reportbuilder/tests/behat/customreports.feature b/reportbuilder/tests/behat/customreports.feature index d69bdfe7edc..3cccb734b08 100644 --- a/reportbuilder/tests/behat/customreports.feature +++ b/reportbuilder/tests/behat/customreports.feature @@ -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 | 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: diff --git a/reportbuilder/tests/external/system_report_data_exporter_test.php b/reportbuilder/tests/external/system_report_data_exporter_test.php index 881a97bb213..637615c49f1 100644 --- a/reportbuilder/tests/external/system_report_data_exporter_test.php +++ b/reportbuilder/tests/external/system_report_data_exporter_test.php @@ -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); diff --git a/reportbuilder/tests/external/systemreports/retrieve_test.php b/reportbuilder/tests/external/systemreports/retrieve_test.php index 0b0d151d2a3..0a410ad6bf0 100644 --- a/reportbuilder/tests/external/systemreports/retrieve_test.php +++ b/reportbuilder/tests/external/systemreports/retrieve_test.php @@ -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); diff --git a/reportbuilder/tests/generator/lib.php b/reportbuilder/tests/generator/lib.php index dc7cbdf049c..e83d6e2d1c4 100644 --- a/reportbuilder/tests/generator/lib.php +++ b/reportbuilder/tests/generator/lib.php @@ -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); diff --git a/reportbuilder/tests/generator_test.php b/reportbuilder/tests/generator_test.php index f867a6f41b6..1c9a13f6033 100644 --- a/reportbuilder/tests/generator_test.php +++ b/reportbuilder/tests/generator_test.php @@ -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'))); } /** diff --git a/reportbuilder/tests/lib_test.php b/reportbuilder/tests/lib_test.php new file mode 100644 index 00000000000..1a01a141157 --- /dev/null +++ b/reportbuilder/tests/lib_test.php @@ -0,0 +1,78 @@ +. + +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 + * @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); + } +} diff --git a/reportbuilder/tests/local/filters/tags_test.php b/reportbuilder/tests/local/filters/tags_test.php index a3148673c08..9d42c591eb8 100644 --- a/reportbuilder/tests/local/filters/tags_test.php +++ b/reportbuilder/tests/local/filters/tags_test.php @@ -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); + } } diff --git a/reportbuilder/tests/local/helpers/report_test.php b/reportbuilder/tests/local/helpers/report_test.php index 1eceae0f5ce..dcb198f935c 100644 --- a/reportbuilder/tests/local/helpers/report_test.php +++ b/reportbuilder/tests/local/helpers/report_test.php @@ -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'))); diff --git a/reportbuilder/upgrade.txt b/reportbuilder/upgrade.txt index c4ab09ab63c..08c38f8fb39 100644 --- a/reportbuilder/upgrade.txt +++ b/reportbuilder/upgrade.txt @@ -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 diff --git a/version.php b/version.php index 8ccfeb3a1d7..45ff50e05d4 100644 --- a/version.php +++ b/version.php @@ -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