diff --git a/badges/tests/reportbuilder/datasource/badges_test.php b/badges/tests/reportbuilder/datasource/badges_test.php index fc7d60ceb74..7687a8ca7cf 100644 --- a/badges/tests/reportbuilder/datasource/badges_test.php +++ b/badges/tests/reportbuilder/datasource/badges_test.php @@ -94,44 +94,4 @@ class badges_test extends core_reportbuilder_testcase { return array_values($row); }, $content)); } - - /** - * Test datasource using course/user entities that each contain tags - */ - public function test_datasource_course_user_tags(): void { - $this->resetAfterTest(); - $this->setAdminUser(); - - $course = $this->getDataGenerator()->create_course(['tags' => ['horse']]); - $user = $this->getDataGenerator()->create_user(['interests' => ['pie']]); - - /** @var core_badges_generator $generator */ - $generator = $this->getDataGenerator()->get_plugin_generator('core_badges'); - - // Create course badge, issue to user. - $badge = $generator->create_badge(['name' => 'Course badge', 'type' => BADGE_TYPE_COURSE, 'courseid' => $course->id]); - $badge->issue($user->id, true); - - /** @var core_reportbuilder_generator $generator */ - $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); - - // Create our report. - $report = $generator->create_report(['name' => 'Badges', 'source' => badges::class, 'default' => 0]); - $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'badge:name']); - $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course:fullname']); - $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course:tags']); - $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:fullname']); - $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:interests']); - - $content = $this->get_custom_report_content($report->get('id')); - - $this->assertCount(1, $content); - $this->assertEquals([ - $badge->name, - $course->fullname, - 'horse', - fullname($user), - 'pie', - ], array_values($content[0])); - } } diff --git a/course/classes/reportbuilder/datasource/courses.php b/course/classes/reportbuilder/datasource/courses.php index 13dd4edf748..8e641b0f731 100644 --- a/course/classes/reportbuilder/datasource/courses.php +++ b/course/classes/reportbuilder/datasource/courses.php @@ -22,6 +22,7 @@ use core_course\local\entities\course_category; use core_reportbuilder\datasource; use core_reportbuilder\local\entities\course; use core_reportbuilder\local\helpers\database; +use core_tag\reportbuilder\local\entities\tag; /** * Courses datasource @@ -63,9 +64,20 @@ class courses extends datasource { ->add_join("JOIN {course_categories} {$coursecattablealias} ON {$coursecattablealias}.id = {$coursetablealias}.category")); + // Join the tag entity. + $tagentity = (new tag()) + ->set_table_alias('tag', $courseentity->get_table_alias('tag')); + $this->add_entity($tagentity + ->add_joins($courseentity->get_tag_joins())); + // Add all columns/filters/conditions from entities to be available in custom reports. $this->add_all_from_entity($coursecatentity->get_entity_name()); $this->add_all_from_entity($courseentity->get_entity_name()); + + // Add specific tag entity elements. + $this->add_columns_from_entity($tagentity->get_entity_name(), ['name', 'namewithlink']); + $this->add_filter($tagentity->get_filter('name')); + $this->add_condition($tagentity->get_condition('name')); } /** diff --git a/course/tests/datasource_test.php b/course/tests/reportbuilder/datasource/courses_test.php similarity index 52% rename from course/tests/datasource_test.php rename to course/tests/reportbuilder/datasource/courses_test.php index 64c2826bc32..77c728f5fc4 100644 --- a/course/tests/datasource_test.php +++ b/course/tests/reportbuilder/datasource/courses_test.php @@ -20,6 +20,7 @@ namespace core_course\reportbuilder\datasource; use core_reportbuilder_testcase; use core_reportbuilder_generator; +use core_reportbuilder\local\filters\tags; defined('MOODLE_INTERNAL') || die(); @@ -27,19 +28,19 @@ global $CFG; require_once("{$CFG->dirroot}/reportbuilder/tests/helpers.php"); /** - * Unit tests for component datasources + * Unit tests for courses datasources * * @package core_course * @covers \core_course\reportbuilder\datasource\courses * @copyright 2021 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class datasource_test extends core_reportbuilder_testcase { +class courses_test extends core_reportbuilder_testcase { /** - * Test courses datasource + * Test default datasource */ - public function test_courses_datasource(): void { + public function test_datasource_default(): void { $this->resetAfterTest(); // Test subject. @@ -53,7 +54,7 @@ class datasource_test extends core_reportbuilder_testcase { /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); - $report = $generator->create_report(['name' => 'Courses', 'source' => courses::class]); + $report = $generator->create_report(['name' => 'Courses', 'source' => courses::class, 'default' => 1]); $content = $this->get_custom_report_content($report->get('id')); $this->assertCount(1, $content); @@ -67,6 +68,38 @@ class datasource_test extends core_reportbuilder_testcase { ], $contentrow); } + /** + * Test datasource columns that aren't added by default + */ + public function test_datasource_non_default_columns(): void { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(['tags' => ['Horses']]); + + /** @var core_reportbuilder_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); + $report = $generator->create_report(['name' => 'Courses', 'source' => courses::class, 'default' => 0]); + + // Category. + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course_category:path']); + + // Course. + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course:fullname']); + + // Tags. + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'tag:name']); + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'tag:namewithlink']); + + $content = $this->get_custom_report_content($report->get('id')); + $this->assertCount(1, $content); + + $courserow = array_values($content[0]); + $this->assertEquals('Category 1', $courserow[0]); + $this->assertEquals($course->fullname, $courserow[1]); + $this->assertEquals('Horses', $courserow[2]); + $this->assertStringContainsString('Horses', $courserow[3]); + } + /** * Tests courses datasource using multilang filters */ @@ -104,4 +137,59 @@ class datasource_test extends core_reportbuilder_testcase { 'Crs (en)', ], $contentrow); } + + /** + * Data provider for {@see test_datasource_filters} + * + * @return array[] + */ + public function datasource_filters_provider(): array { + return [ + // Tags. + 'Filter tag name' => ['tag:name', [ + 'tag:name_operator' => tags::EQUAL_TO, + 'tag:name_value' => [-1], + ], false], + 'Filter tag name not empty' => ['tag:name', [ + 'tag:name_operator' => tags::NOT_EMPTY, + ], true], + ]; + } + + /** + * Test datasource filters + * + * @param string $filtername + * @param array $filtervalues + * @param bool $expectmatch + * + * @dataProvider datasource_filters_provider + */ + public function test_datasource_filters( + string $filtername, + array $filtervalues, + bool $expectmatch + ): void { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(['tags' => ['Horses']]); + + /** @var core_reportbuilder_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); + + // Create report containing single column, and given filter. + $report = $generator->create_report(['name' => 'Tasks', 'source' => courses::class, 'default' => 0]); + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course:fullname']); + + // Add filter, set it's values. + $generator->create_filter(['reportid' => $report->get('id'), 'uniqueidentifier' => $filtername]); + $content = $this->get_custom_report_content($report->get('id'), 0, $filtervalues); + + if ($expectmatch) { + $this->assertCount(1, $content); + $this->assertEquals($course->fullname, reset($content[0])); + } else { + $this->assertEmpty($content); + } + } } diff --git a/lang/en/moodle.php b/lang/en/moodle.php index bd3e25265e8..97e7d7e4a77 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -293,6 +293,8 @@ $string['contentexport_footersummary'] = 'This file is part of the content downl $string['contentexport_modulesummary'] = 'This page is part of the content downloaded from {$a->modulename} on {$a->date}. Note that some content and any files larger than {$a->maxfilesize} are not downloaded.'; $string['contentexport_viewfilename'] = 'View the file {$a}'; $string['contentbank'] = 'Content bank'; +$string['context'] = 'Context'; +$string['contexturl'] = 'Context URL'; $string['continue'] = 'Continue'; $string['continuetocourse'] = 'Click here to enter your course'; $string['convertingwikitomarkdown'] = 'Converting wiki to Markdown'; diff --git a/lang/en/tag.php b/lang/en/tag.php index 4507a97db06..441e4a217f6 100644 --- a/lang/en/tag.php +++ b/lang/en/tag.php @@ -75,7 +75,9 @@ $string['combineselected'] = 'Combine selected'; $string['id'] = 'id'; $string['inalltagcoll'] = 'Everywhere'; $string['inputstandardtags'] = 'Enter comma-separated list of new tags'; +$string['itemid'] = 'Item ID'; $string['itemstaggedwith'] = '{$a->tagarea} tagged with "{$a->tag}"'; +$string['itemtype'] = 'Item type'; $string['lesstags'] = 'less...'; $string['managestandardtags'] = 'Manage standard tags'; $string['managetags'] = 'Manage tags'; @@ -84,6 +86,7 @@ $string['moretags'] = 'more...'; $string['name'] = 'Tag name'; $string['namesalreadybeeingused'] = 'Tag names already being used'; $string['nameuseddocombine'] = 'The tag name is already in use. Do you want to combine these tags?'; +$string['namewithlink'] = 'Tag name with link'; $string['newcollnamefor'] = 'New name for tag collection {$a}'; $string['newnamefor'] = 'New name for tag {$a}'; $string['nextpage'] = 'More'; @@ -137,6 +140,7 @@ $string['standardsuggest'] = 'Suggest'; $string['standardtag'] = 'Standard'; $string['suredeletecoll'] = 'Are you sure you want to delete tag collection "{$a}"?'; $string['tag'] = 'Tag'; +$string['tagarea'] = 'Tag area'; $string['tagarea_blog_external'] = 'External blog posts'; $string['tagarea_post'] = 'Blog posts'; $string['tagarea_user'] = 'User interests'; @@ -145,10 +149,12 @@ $string['tagarea_course_modules'] = 'Activities and resources'; $string['tagareaenabled'] = 'Enabled'; $string['tagareaname'] = 'Name'; $string['tagareas'] = 'Tag areas'; +$string['tagauthor'] = 'Tag author'; $string['tagcollection'] = 'Tag collection'; $string['tagcollection_help'] = 'Tag collections are sets of tags for different areas. For example, a collection of standard tags can be used to tag courses, with user interests and blog post tags kept in a separate collection. When a user clicks on a tag, the tag page displays only items with that tag in the same collection. Tags can be automatically added to a collection according to the area tagged or can be added manually as standard tags.'; $string['tagcollections'] = 'Tag collections'; $string['tagdescription'] = 'Tag description'; +$string['taginstance'] = 'Tag instance'; $string['tags'] = 'Tags'; $string['tagsaredisabled'] = 'Tags are disabled'; $string['thingstaggedwith'] = '"{$a->name}" is used {$a->count} times'; diff --git a/reportbuilder/classes/local/entities/course.php b/reportbuilder/classes/local/entities/course.php index f897c8476a8..3a01a16b436 100644 --- a/reportbuilder/classes/local/entities/course.php +++ b/reportbuilder/classes/local/entities/course.php @@ -20,12 +20,10 @@ namespace core_reportbuilder\local\entities; use context_course; use context_helper; -use core_tag_tag; use core_reportbuilder\local\filters\boolean_select; use core_reportbuilder\local\filters\course_selector; use core_reportbuilder\local\filters\date; use core_reportbuilder\local\filters\select; -use core_reportbuilder\local\filters\tags; use core_reportbuilder\local\filters\text; use core_reportbuilder\local\helpers\custom_fields; use core_reportbuilder\local\helpers\format; @@ -202,7 +200,7 @@ class course extends base { * * @return string[] */ - private function get_tag_joins(): array { + public function get_tag_joins(): array { $course = $this->get_table_alias('course'); $taginstance = $this->get_table_alias('tag_instance'); $tag = $this->get_table_alias('tag'); @@ -296,22 +294,6 @@ class course extends base { $columns[] = $column; } - // Tags. - $tag = $this->get_table_alias('tag'); - $columns[] = (new column( - 'tags', - new lang_string('tags'), - $this->get_entity_name() - )) - ->add_joins($this->get_joins()) - ->add_joins($this->get_tag_joins()) - ->set_type(column::TYPE_TEXT) - ->add_fields("{$tag}.name, {$tag}.rawname") - ->set_is_sortable(true) - ->add_callback(static function($value, stdClass $tag): string { - return core_tag_tag::make_display_name($tag); - }); - return $columns; } @@ -363,22 +345,6 @@ class course extends base { $filters[] = $filter; } - // Tags. - $tag = $this->get_table_alias('tag'); - $filters[] = (new filter( - tags::class, - 'tags', - new lang_string('tags'), - $this->get_entity_name(), - "{$tag}.id" - )) - ->add_joins($this->get_joins()) - ->add_joins($this->get_tag_joins()) - ->set_options([ - 'component' => 'core', - 'itemtype' => 'course', - ]); - // We add our own custom course selector filter. $filters[] = (new filter( course_selector::class, diff --git a/reportbuilder/classes/local/entities/user.php b/reportbuilder/classes/local/entities/user.php index 0374260d527..fa9e371f4f1 100644 --- a/reportbuilder/classes/local/entities/user.php +++ b/reportbuilder/classes/local/entities/user.php @@ -22,7 +22,6 @@ use context_helper; use context_system; use context_user; use core_component; -use core_tag_tag; use html_writer; use lang_string; use moodle_url; @@ -31,7 +30,6 @@ use core_user\fields; use core_reportbuilder\local\filters\boolean_select; use core_reportbuilder\local\filters\date; use core_reportbuilder\local\filters\select; -use core_reportbuilder\local\filters\tags; use core_reportbuilder\local\filters\text; use core_reportbuilder\local\filters\user as user_filter; use core_reportbuilder\local\helpers\user_profile_fields; @@ -143,7 +141,7 @@ class user extends base { * * @return string[] */ - private function get_tag_joins(): array { + public function get_tag_joins(): array { $user = $this->get_table_alias('user'); $taginstance = $this->get_table_alias('tag_instance'); $tag = $this->get_table_alias('tag'); @@ -272,22 +270,6 @@ class user extends base { return !empty($row->id) ? $OUTPUT->user_picture($row, ['link' => false, 'alttext' => false]) : ''; }); - // Interests (tags). - $tag = $this->get_table_alias('tag'); - $columns[] = (new column( - 'interests', - new lang_string('interests'), - $this->get_entity_name() - )) - ->add_joins($this->get_joins()) - ->add_joins($this->get_tag_joins()) - ->set_type(column::TYPE_TEXT) - ->add_fields("{$tag}.name, {$tag}.rawname") - ->set_is_sortable(true) - ->add_callback(static function($value, stdClass $tag): string { - return core_tag_tag::make_display_name($tag); - }); - // Add all other user fields. $userfields = $this->get_user_fields(); foreach ($userfields as $userfield => $userfieldlang) { @@ -529,22 +511,6 @@ class user extends base { $filters[] = $filter; } - // Interests (tags). - $tag = $this->get_table_alias('tag'); - $filters[] = (new filter( - tags::class, - 'interests', - new lang_string('interests'), - $this->get_entity_name(), - "{$tag}.id" - )) - ->add_joins($this->get_joins()) - ->add_joins($this->get_tag_joins()) - ->set_options([ - 'component' => 'core', - 'itemtype' => 'user', - ]); - // User select filter. $filters[] = (new filter( user_filter::class, diff --git a/reportbuilder/classes/local/filters/tags.php b/reportbuilder/classes/local/filters/tags.php index 6a71729a5a5..e8ff43616bc 100644 --- a/reportbuilder/classes/local/filters/tags.php +++ b/reportbuilder/classes/local/filters/tags.php @@ -30,11 +30,6 @@ use core_reportbuilder\local\helpers\database; * * The field SQL should be the field containing the ID of the {tag} table * - * The following array properties must be passed to the {@see \core_reportbuilder\local\report\filter::set_options} method when - * defining this filter, to define the component/itemtype you are using for tags: - * - * ['component' => 'core', 'itemtype' => 'user'] - * * @package core_reportbuilder * @copyright 2022 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later @@ -69,30 +64,22 @@ class tags extends base { * Setup form * * @param MoodleQuickForm $mform - * @throws coding_exception If component/itemtype options are missing */ public function setup_form(MoodleQuickForm $mform): void { global $DB; - $options = $this->filter->get_options(); - if (!array_key_exists('component', $options) || !array_key_exists('itemtype', $options)) { - throw new coding_exception('Missing \'component\' and/or \'itemtype\' in filter options'); - } - $operatorlabel = get_string('filterfieldoperator', 'core_reportbuilder', $this->get_header()); $mform->addElement('select', "{$this->name}_operator", $operatorlabel, $this->get_operators()) ->setHiddenLabel(true); $sql = 'SELECT DISTINCT t.id, t.name, t.rawname FROM {tag} t - JOIN {tag_instance} ti ON ti.tagid = t.id - WHERE ti.component = :component AND ti.itemtype = :itemtype 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, ['component' => $options['component'], 'itemtype' => $options['itemtype']])); + }, $DB->get_records_sql($sql)); $valuelabel = get_string('filterfieldvalue', 'core_reportbuilder', $this->get_header()); $mform->addElement('autocomplete', "{$this->name}_value", $valuelabel, $tags, ['multiple' => true]) diff --git a/reportbuilder/tests/behat/conditioneditor.feature b/reportbuilder/tests/behat/conditioneditor.feature index 00a3b6b5a27..2295ca5daf9 100644 --- a/reportbuilder/tests/behat/conditioneditor.feature +++ b/reportbuilder/tests/behat/conditioneditor.feature @@ -35,13 +35,13 @@ Feature: Manage custom report conditions Scenario: Add tags condition to report Given the following "core_reportbuilder > Condition" exists: - | report | My report | - | uniqueidentifier | user:interests | + | report | My report | + | uniqueidentifier | tag:name | And I am on the "My report" "reportbuilder > Editor" page logged in as "admin" When I click on "Show/hide 'Conditions'" "button" - And I set the following fields in the "Interests" "core_reportbuilder > Condition" to these values: - | Interests operator | Is equal to | - | Interests value | dancing | + And I set the following fields in the "Tag name" "core_reportbuilder > Condition" to these values: + | Tag name operator | Is equal to | + | Tag name value | dancing | And I click on "Apply" "button" in the "[data-region='settings-conditions']" "css_element" Then I should see "Conditions applied" And I should see "User One" in the "reportbuilder-table" "table" diff --git a/reportbuilder/tests/external/custom_report_column_cards_exporter_test.php b/reportbuilder/tests/external/custom_report_column_cards_exporter_test.php index 32d444ee650..105d90d06d1 100644 --- a/reportbuilder/tests/external/custom_report_column_cards_exporter_test.php +++ b/reportbuilder/tests/external/custom_report_column_cards_exporter_test.php @@ -50,8 +50,9 @@ class custom_report_column_cards_exporter_test extends advanced_testcase { $exporter = new custom_report_column_cards_exporter(null, ['report' => $reportinstance]); $export = $exporter->export($PAGE->get_renderer('core_reportbuilder')); - $this->assertCount(2, $export->menucards); - [$menucardcategory, $menucardcourse] = $export->menucards; + // The root of the menu cards property should contain each entity. + $this->assertCount(3, $export->menucards); + [$menucardcategory, $menucardcourse, $menucardtag] = $export->menucards; // Course category entity menu card. $this->assertEquals('Course category', $menucardcategory['name']); @@ -80,5 +81,19 @@ class custom_report_column_cards_exporter_test extends advanced_testcase { 'title' => 'Add column \'Course full name with link\'', 'action' => 'report-add-column', ], $menucarditem); + + // Tag entity menu card. + $this->assertEquals('Tag', $menucardtag['name']); + $this->assertEquals('tag', $menucardtag['key']); + $this->assertNotEmpty($menucardtag['items']); + + // Test the structure of the first menu card item. + $menucarditem = reset($menucardtag['items']); + $this->assertEquals([ + 'name' => 'Tag name', + 'identifier' => 'tag:name', + 'title' => 'Add column \'Tag name\'', + 'action' => 'report-add-column', + ], $menucarditem); } } diff --git a/reportbuilder/tests/external/custom_report_conditions_exporter_test.php b/reportbuilder/tests/external/custom_report_conditions_exporter_test.php index 5a55f01d526..c5c2c44841e 100644 --- a/reportbuilder/tests/external/custom_report_conditions_exporter_test.php +++ b/reportbuilder/tests/external/custom_report_conditions_exporter_test.php @@ -51,11 +51,11 @@ class custom_report_conditions_exporter_test extends advanced_testcase { $exporter = new custom_report_conditions_exporter(null, ['report' => $reportinstance]); $export = $exporter->export($PAGE->get_renderer('core_reportbuilder')); - // The root of the available conditions property should contain two entities. $this->assertTrue($export->hasavailableconditions); - $this->assertCount(2, $export->availableconditions); - [$conditionscategory, $conditionscourse] = $export->availableconditions; + // The root of the available conditions property should contain each entity. + $this->assertCount(3, $export->availableconditions); + [$conditionscategory, $conditionscourse, $conditionstag] = $export->availableconditions; // Course category conditions, assert structure of first item. $this->assertEquals('Course category', $conditionscategory['optiongroup']['text']); @@ -76,6 +76,14 @@ class custom_report_conditions_exporter_test extends advanced_testcase { // Make sure the active condition we added, isn't present in available conditions. $this->assertNotContains('course:shortname', array_column($conditionscourse['optiongroup']['values'], 'value')); + // Tag conditions, assert structure of first item. + $this->assertEquals('Tag', $conditionstag['optiongroup']['text']); + $this->assertGreaterThanOrEqual(1, $conditionstag['optiongroup']['values']); + $this->assertEquals([ + 'value' => 'tag:name', + 'visiblename' => 'Tag name', + ], $conditionstag['optiongroup']['values'][0]); + // The active conditions are contained inside form HTML, just assert there's something present. $this->assertTrue($export->hasactiveconditions); $this->assertNotEmpty($export->activeconditionsform); diff --git a/reportbuilder/tests/external/custom_report_filters_exporter_test.php b/reportbuilder/tests/external/custom_report_filters_exporter_test.php index 8f76cd408eb..28c17675939 100644 --- a/reportbuilder/tests/external/custom_report_filters_exporter_test.php +++ b/reportbuilder/tests/external/custom_report_filters_exporter_test.php @@ -64,11 +64,11 @@ class custom_report_filters_exporter_test extends advanced_testcase { $exporter = new custom_report_filters_exporter(null, ['report' => $reportinstance]); $export = $exporter->export($PAGE->get_renderer('core_reportbuilder')); - // The root of the available filters property should contain two entities. $this->assertTrue($export->hasavailablefilters); - $this->assertCount(2, $export->availablefilters); - [$filterscategory, $filterscourse] = $export->availablefilters; + // The root of the available filters property should contain each entity. + $this->assertCount(3, $export->availablefilters); + [$filterscategory, $filterscourse, $filterstag] = $export->availablefilters; // Course category filters, assert structure of first item. $this->assertEquals('Course category', $filterscategory['optiongroup']['text']); @@ -91,6 +91,14 @@ class custom_report_filters_exporter_test extends advanced_testcase { $this->assertNotContains('course_category:name', $filterscourseavailable); $this->assertNotContains('course:idnumber', $filterscourseavailable); + // Tag filters, assert structure of first item. + $this->assertEquals('Tag', $filterstag['optiongroup']['text']); + $this->assertGreaterThanOrEqual(1, $filterstag['optiongroup']['values']); + $this->assertEquals([ + 'value' => 'tag:name', + 'visiblename' => 'Tag name', + ], $filterstag['optiongroup']['values'][0]); + $this->assertTrue($export->hasactivefilters); $this->assertCount(2, $export->activefilters); [$activefiltercourseidnumber, $activefiltercategoryname] = $export->activefilters; diff --git a/reportbuilder/tests/local/entities/course_test.php b/reportbuilder/tests/local/entities/course_test.php index b445508a53c..6e769178e3b 100644 --- a/reportbuilder/tests/local/entities/course_test.php +++ b/reportbuilder/tests/local/entities/course_test.php @@ -156,7 +156,6 @@ class course_test extends advanced_testcase { $this->assertEquals('Gregorian', $courserow['calendartype']); $this->assertEquals('afterburner', $courserow['theme']); $this->assertEquals(get_string_manager()->get_list_of_translations()['en'], $courserow['lang']); - $this->assertEquals('dancing', $courserow['tags']); $expected = 'Course 1'; $this->assertEquals($expected, $courserow['coursefullnamewithlink']); $expected = 'C1'; @@ -248,15 +247,6 @@ class course_test extends advanced_testcase { $this->assertEquals([ 'Course 1', ], array_column($tablerows, 'fullname')); - - // Filter by tags field. - $tablerows = $this->get_report_table_rows([ - 'course:tags_operator' => tags::EQUAL_TO, - 'course:tags_value' => [ - $DB->get_field('tag', 'id', ['name' => 'dancing'], MUST_EXIST), - ], - ]); - $this->assertEquals(['Course 1'], array_column($tablerows, 'fullname')); } /** diff --git a/reportbuilder/tests/local/entities/user_test.php b/reportbuilder/tests/local/entities/user_test.php index 1e3e063c3ca..ae45d358778 100644 --- a/reportbuilder/tests/local/entities/user_test.php +++ b/reportbuilder/tests/local/entities/user_test.php @@ -85,7 +85,6 @@ class user_test extends advanced_testcase { $this->assertEquals('Yes', $userrow['suspended']); $this->assertEquals('No', $userrow['confirmed']); $this->assertEquals('Spain', $userrow['country']); - $this->assertEquals('dancing', $userrow['interests']); $this->assertEquals('Blue', $userrow['profilefield_favcolor']); $this->assertEquals('Time travel', $userrow['profilefield_favsuperpower']); } @@ -230,15 +229,6 @@ class user_test extends advanced_testcase { $this->assertEquals([ 'Daffy Duck', ], array_column($tablerows, 'fullname')); - - // Filter by interests (tags) field. - $tablerows = $this->get_report_table_rows([ - 'user:interests_operator' => tags::EQUAL_TO, - 'user:interests_value' => [ - $DB->get_field('tag', 'id', ['name' => 'dancing'], MUST_EXIST), - ], - ]); - $this->assertEquals(['Daffy Duck'], array_column($tablerows, 'fullname')); } /** diff --git a/tag/classes/reportbuilder/datasource/tags.php b/tag/classes/reportbuilder/datasource/tags.php new file mode 100644 index 00000000000..18b693bec23 --- /dev/null +++ b/tag/classes/reportbuilder/datasource/tags.php @@ -0,0 +1,119 @@ +. + +declare(strict_types=1); + +namespace core_tag\reportbuilder\datasource; + +use lang_string; +use core_reportbuilder\datasource; +use core_reportbuilder\local\entities\user; +use core_tag\reportbuilder\local\entities\{collection, tag, instance}; + +/** + * Tags datasource + * + * @package core_tag + * @copyright 2022 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class tags extends datasource { + + /** + * Return user friendly name of the report source + * + * @return string + */ + public static function get_name(): string { + return get_string('tags', 'core_tag'); + } + + /** + * Initialise report + */ + protected function initialise(): void { + $collectionentity = new collection(); + + $collectionalias = $collectionentity->get_table_alias('tag_coll'); + $this->set_main_table('tag_coll', $collectionalias); + + $this->add_entity($collectionentity); + + // Join tag entity to collection. + $tagentity = new tag(); + $tagalias = $tagentity->get_table_alias('tag'); + $this->add_entity($tagentity + ->add_join("LEFT JOIN {tag} {$tagalias} ON {$tagalias}.tagcollid = {$collectionalias}.id") + ); + + // Join instance entity to tag. + $instanceentity = new instance(); + $instancealias = $instanceentity->get_table_alias('tag_instance'); + $this->add_entity($instanceentity + ->add_joins($tagentity->get_joins()) + ->add_join("LEFT JOIN {tag_instance} {$instancealias} ON {$instancealias}.tagid = {$tagalias}.id") + ); + + // Join user entity to represent the tag author. + $userentity = (new user()) + ->set_entity_title(new lang_string('tagauthor', 'core_tag')); + $useralias = $userentity->get_table_alias('user'); + $this->add_entity($userentity + ->add_joins($tagentity->get_joins()) + ->add_join("LEFT JOIN {user} {$useralias} ON {$useralias}.id = {$tagalias}.userid") + ); + + // Add report elements from each of the entities we added to the report. + $this->add_all_from_entities(); + } + + /** + * Return the columns that will be added to the report upon creation + * + * @return string[] + */ + public function get_default_columns(): array { + return [ + 'collection:name', + 'tag:namewithlink', + 'tag:standard', + 'instance:context', + ]; + } + + /** + * Return the filters that will be added to the report upon creation + * + * @return string[] + */ + public function get_default_filters(): array { + return [ + 'tag:name', + 'tag:standard', + ]; + } + + /** + * Return the conditions that will be added to the report upon creation + * + * @return string[] + */ + public function get_default_conditions(): array { + return [ + 'collection:name', + ]; + } +} diff --git a/tag/classes/reportbuilder/local/entities/collection.php b/tag/classes/reportbuilder/local/entities/collection.php new file mode 100644 index 00000000000..05c8f81ea7e --- /dev/null +++ b/tag/classes/reportbuilder/local/entities/collection.php @@ -0,0 +1,198 @@ +. + +declare(strict_types=1); + +namespace core_tag\reportbuilder\local\entities; + +use core_tag_collection; +use lang_string; +use stdClass; +use core_reportbuilder\local\entities\base; +use core_reportbuilder\local\filters\{boolean_select, select}; +use core_reportbuilder\local\helpers\format; +use core_reportbuilder\local\report\{column, filter}; + +/** + * Tag collection entity + * + * @package core_tag + * @copyright 2022 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class collection extends base { + + /** + * Database tables that this entity uses and their default aliases + * + * @return array + */ + protected function get_default_table_aliases(): array { + return ['tag_coll' => 'tc']; + } + + /** + * The default title for this entity + * + * @return lang_string + */ + protected function get_default_entity_title(): lang_string { + return new lang_string('tagcollection', 'core_tag'); + } + + /** + * Initialise the entity + * + * @return base + */ + public function initialise(): base { + $columns = $this->get_all_columns(); + foreach ($columns as $column) { + $this->add_column($column); + } + + // All the filters defined by the entity can also be used as conditions. + $filters = $this->get_all_filters(); + foreach ($filters as $filter) { + $this + ->add_filter($filter) + ->add_condition($filter); + } + + return $this; + } + + /** + * Returns list of all available columns + * + * @return column[] + */ + protected function get_all_columns(): array { + $collectionalias = $this->get_table_alias('tag_coll'); + + // Name. + $columns[] = (new column( + 'name', + new lang_string('name'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_TEXT) + ->add_fields("{$collectionalias}.name, {$collectionalias}.component, {$collectionalias}.isdefault, + {$collectionalias}.id") + ->set_is_sortable(true) + ->add_callback(static function(?string $name, stdClass $collection): string { + return core_tag_collection::display_name($collection); + }); + + // Default. + $columns[] = (new column( + 'default', + new lang_string('defautltagcoll', 'core_tag'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_BOOLEAN) + ->add_fields("{$collectionalias}.isdefault") + ->set_is_sortable(true) + ->add_callback([format::class, 'boolean_as_text']); + + // Component. + $columns[] = (new column( + 'component', + new lang_string('component', 'core_tag'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_TEXT) + ->add_fields("{$collectionalias}.component") + ->set_is_sortable(true); + + // Searchable. + $columns[] = (new column( + 'searchable', + new lang_string('searchable', 'core_tag'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_BOOLEAN) + ->add_fields("{$collectionalias}.searchable") + ->set_is_sortable(true) + ->add_callback([format::class, 'boolean_as_text']); + + // Custom URL. + $columns[] = (new column( + 'customurl', + new lang_string('url'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_TEXT) + ->add_fields("{$collectionalias}.customurl") + ->set_is_sortable(true); + + return $columns; + } + + /** + * Return list of all available filters + * + * @return filter[] + */ + protected function get_all_filters(): array { + $collectionalias = $this->get_table_alias('tag_coll'); + + // Name. + $filters[] = (new filter( + select::class, + 'name', + new lang_string('name'), + $this->get_entity_name(), + "{$collectionalias}.id" + )) + ->add_joins($this->get_joins()) + ->set_options_callback(static function(): array { + global $DB; + + $collections = $DB->get_records('tag_coll', [], 'sortorder', 'id, name, component, isdefault'); + return array_map(static function(stdClass $collection): string { + return core_tag_collection::display_name($collection); + }, $collections); + }); + + // Default. + $filters[] = (new filter( + boolean_select::class, + 'default', + new lang_string('defautltagcoll', 'core_tag'), + $this->get_entity_name(), + "{$collectionalias}.isdefault" + )) + ->add_joins($this->get_joins()); + + // Searchable. + $filters[] = (new filter( + boolean_select::class, + 'searchable', + new lang_string('searchable', 'core_tag'), + $this->get_entity_name(), + "{$collectionalias}.searchable" + )) + ->add_joins($this->get_joins()); + + return $filters; + } +} diff --git a/tag/classes/reportbuilder/local/entities/instance.php b/tag/classes/reportbuilder/local/entities/instance.php new file mode 100644 index 00000000000..818d51554d6 --- /dev/null +++ b/tag/classes/reportbuilder/local/entities/instance.php @@ -0,0 +1,283 @@ +. + +declare(strict_types=1); + +namespace core_tag\reportbuilder\local\entities; + +use context; +use context_helper; +use core_collator; +use core_tag_area; +use html_writer; +use lang_string; +use stdClass; +use core_reportbuilder\local\entities\base; +use core_reportbuilder\local\filters\{date, select}; +use core_reportbuilder\local\helpers\format; +use core_reportbuilder\local\report\{column, filter}; + +/** + * Tag instance entity + * + * @package core_tag + * @copyright 2022 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class instance extends base { + + /** + * Database tables that this entity uses and their default aliases + * + * @return array + */ + protected function get_default_table_aliases(): array { + return [ + 'tag_instance' => 'ti', + 'context' => 'tictx', + ]; + } + + /** + * The default title for this entity + * + * @return lang_string + */ + protected function get_default_entity_title(): lang_string { + return new lang_string('taginstance', 'core_tag'); + } + + /** + * Initialise the entity + * + * @return base + */ + public function initialise(): base { + $columns = $this->get_all_columns(); + foreach ($columns as $column) { + $this->add_column($column); + } + + // All the filters defined by the entity can also be used as conditions. + $filters = $this->get_all_filters(); + foreach ($filters as $filter) { + $this + ->add_filter($filter) + ->add_condition($filter); + } + + return $this; + } + + /** + * Returns list of all available columns + * + * @return column[] + */ + protected function get_all_columns(): array { + $instancealias = $this->get_table_alias('tag_instance'); + $contextalias = $this->get_table_alias('context'); + + // Area. + $columns[] = (new column( + 'area', + new lang_string('tagarea', 'core_tag'), + $this->get_entity_name() + + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_TEXT) + ->add_fields("{$instancealias}.component, {$instancealias}.itemtype") + ->set_is_sortable(true, ["{$instancealias}.component", "{$instancealias}.itemtype"]) + ->add_callback(static function($component, stdClass $area): string { + if ($component === null) { + return ''; + } + return (string) core_tag_area::display_name($area->component, $area->itemtype); + }); + + // Context. + $columns[] = (new column( + 'context', + new lang_string('context'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_TEXT) + ->add_join("LEFT JOIN {context} {$contextalias} ON {$contextalias}.id = {$instancealias}.contextid") + ->add_fields("{$instancealias}.contextid, " . context_helper::get_preload_record_columns_sql($contextalias)) + // Sorting may not order alphabetically, but will at least group contexts together. + ->set_is_sortable(true) + ->add_callback(static function($contextid, stdClass $context): string { + if ($contextid === null) { + return ''; + } + + context_helper::preload_from_record($context); + return context::instance_by_id($contextid)->get_context_name(); + }); + + // Context URL. + $columns[] = (new column( + 'contexturl', + new lang_string('contexturl'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_TEXT) + ->add_join("LEFT JOIN {context} {$contextalias} ON {$contextalias}.id = {$instancealias}.contextid") + ->add_fields("{$instancealias}.contextid, " . context_helper::get_preload_record_columns_sql($contextalias)) + // Sorting may not order alphabetically, but will at least group contexts together. + ->set_is_sortable(true) + ->add_callback(static function($contextid, stdClass $context): string { + if ($contextid === null) { + return ''; + } + + context_helper::preload_from_record($context); + $context = context::instance_by_id($contextid); + + return html_writer::link($context->get_url(), $context->get_context_name()); + }); + + // Component. + $columns[] = (new column( + 'component', + new lang_string('component', 'core_tag'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_TEXT) + ->add_fields("{$instancealias}.component") + ->set_is_sortable(true); + + // Item type. + $columns[] = (new column( + 'itemtype', + new lang_string('itemtype', 'core_tag'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_TEXT) + ->add_fields("{$instancealias}.itemtype") + ->set_is_sortable(true); + + // Item ID. + $columns[] = (new column( + 'itemid', + new lang_string('itemid', 'core_tag'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_INTEGER) + ->add_fields("{$instancealias}.itemid") + ->set_is_sortable(true) + ->set_disabled_aggregation_all(); + + // Time created. + $columns[] = (new column( + 'timecreated', + new lang_string('timecreated', 'core_reportbuilder'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_TIMESTAMP) + ->add_fields("{$instancealias}.timecreated") + ->set_is_sortable(true) + ->add_callback([format::class, 'userdate']); + + // Time modified. + $columns[] = (new column( + 'timemodified', + new lang_string('timemodified', 'core_reportbuilder'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_TIMESTAMP) + ->add_fields("{$instancealias}.timemodified") + ->set_is_sortable(true) + ->add_callback([format::class, 'userdate']); + + return $columns; + } + + /** + * Return list of all available filters + * + * @return filter[] + */ + protected function get_all_filters(): array { + global $DB; + + $instancealias = $this->get_table_alias('tag_instance'); + + // Area. + $filters[] = (new filter( + select::class, + 'area', + new lang_string('tagarea', 'core_tag'), + $this->get_entity_name(), + $DB->sql_concat("{$instancealias}.component", "'/'", "{$instancealias}.itemtype") + )) + ->add_joins($this->get_joins()) + ->set_options_callback(static function(): array { + $options = []; + foreach (core_tag_area::get_areas() as $areas) { + foreach ($areas as $area) { + $options["{$area->component}/{$area->itemtype}"] = core_tag_area::display_name( + $area->component, $area->itemtype); + } + } + + core_collator::asort($options); + return $options; + }); + + // Time created. + $filters[] = (new filter( + date::class, + 'timecreated', + new lang_string('timecreated', 'core_reportbuilder'), + $this->get_entity_name(), + "{$instancealias}.timecreated" + )) + ->add_joins($this->get_joins()) + ->set_limited_operators([ + date::DATE_ANY, + date::DATE_CURRENT, + date::DATE_LAST, + date::DATE_RANGE, + ]); + + // Time modified. + $filters[] = (new filter( + date::class, + 'timemodified', + new lang_string('timemodified', 'core_reportbuilder'), + $this->get_entity_name(), + "{$instancealias}.timemodified" + )) + ->add_joins($this->get_joins()) + ->set_limited_operators([ + date::DATE_ANY, + date::DATE_CURRENT, + date::DATE_LAST, + date::DATE_RANGE, + ]); + + return $filters; + } +} diff --git a/tag/classes/reportbuilder/local/entities/tag.php b/tag/classes/reportbuilder/local/entities/tag.php new file mode 100644 index 00000000000..f15d18691f7 --- /dev/null +++ b/tag/classes/reportbuilder/local/entities/tag.php @@ -0,0 +1,242 @@ +. + +declare(strict_types=1); + +namespace core_tag\reportbuilder\local\entities; + +use context_system; +use core_tag_tag; +use html_writer; +use lang_string; +use stdClass; +use core_reportbuilder\local\entities\base; +use core_reportbuilder\local\filters\{boolean_select, date, tags}; +use core_reportbuilder\local\helpers\format; +use core_reportbuilder\local\report\{column, filter}; + +/** + * Tag entity + * + * @package core_tag + * @copyright 2022 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class tag extends base { + + /** + * Database tables that this entity uses and their default aliases + * + * @return array + */ + protected function get_default_table_aliases(): array { + return ['tag' => 't']; + } + + /** + * The default title for this entity + * + * @return lang_string + */ + protected function get_default_entity_title(): lang_string { + return new lang_string('tag', 'core_tag'); + } + + /** + * Initialise the entity + * + * @return base + */ + public function initialise(): base { + $columns = $this->get_all_columns(); + foreach ($columns as $column) { + $this->add_column($column); + } + + // All the filters defined by the entity can also be used as conditions. + $filters = $this->get_all_filters(); + foreach ($filters as $filter) { + $this + ->add_filter($filter) + ->add_condition($filter); + } + + return $this; + } + + /** + * Returns list of all available columns + * + * @return column[] + */ + protected function get_all_columns(): array { + $tagalias = $this->get_table_alias('tag'); + + // Name. + $columns[] = (new column( + 'name', + new lang_string('name', 'core_tag'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_TEXT) + ->add_fields("{$tagalias}.rawname, {$tagalias}.name") + ->set_is_sortable(true) + ->add_callback(static function($rawname, stdClass $tag): string { + if ($rawname === null) { + return ''; + } + return core_tag_tag::make_display_name($tag); + }); + + // Name with link. + $columns[] = (new column( + 'namewithlink', + new lang_string('namewithlink', 'core_tag'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_TEXT) + ->add_fields("{$tagalias}.rawname, {$tagalias}.name, {$tagalias}.tagcollid") + ->set_is_sortable(true) + ->add_callback(static function($rawname, stdClass $tag): string { + if ($rawname === null) { + return ''; + } + return html_writer::link(core_tag_tag::make_url($tag->tagcollid, $tag->rawname), + core_tag_tag::make_display_name($tag)); + }); + + // Description. + $columns[] = (new column( + 'description', + new lang_string('tagdescription', 'core_tag'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_LONGTEXT) + ->add_fields("{$tagalias}.description, {$tagalias}.descriptionformat, {$tagalias}.id") + ->add_callback(static function(?string $description, stdClass $tag): string { + global $CFG; + require_once("{$CFG->libdir}/filelib.php"); + + if ($description === null) { + return ''; + } + + $context = context_system::instance(); + $description = file_rewrite_pluginfile_urls($description, 'pluginfile.php', $context->id, 'tag', + 'description', $tag->id); + + return format_text($description, $tag->descriptionformat, ['context' => $context->id]); + }); + + // Standard. + $columns[] = (new column( + 'standard', + new lang_string('standardtag', 'core_tag'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_BOOLEAN) + ->add_fields("{$tagalias}.isstandard") + ->set_is_sortable(true) + ->add_callback([format::class, 'boolean_as_text']); + + // Flagged. + $columns[] = (new column( + 'flagged', + new lang_string('flagged', 'core_tag'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_BOOLEAN) + ->add_fields("{$tagalias}.flag") + ->set_is_sortable(true) + ->add_callback([format::class, 'boolean_as_text']); + + // Time modified. + $columns[] = (new column( + 'timemodified', + new lang_string('timemodified', 'core_reportbuilder'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_TIMESTAMP) + ->add_fields("{$tagalias}.timemodified") + ->set_is_sortable(true) + ->add_callback([format::class, 'userdate']); + + return $columns; + } + + /** + * Return list of all available filters + * + * @return filter[] + */ + protected function get_all_filters(): array { + $tagalias = $this->get_table_alias('tag'); + + // Name. + $filters[] = (new filter( + tags::class, + 'name', + new lang_string('name', 'core_tag'), + $this->get_entity_name(), + "{$tagalias}.id" + )) + ->add_joins($this->get_joins()); + + // Standard. + $filters[] = (new filter( + boolean_select::class, + 'standard', + new lang_string('standardtag', 'core_tag'), + $this->get_entity_name(), + "{$tagalias}.isstandard" + )) + ->add_joins($this->get_joins()); + + // Flagged. + $filters[] = (new filter( + boolean_select::class, + 'flagged', + new lang_string('flagged', 'core_tag'), + $this->get_entity_name(), + "{$tagalias}.flag" + )) + ->add_joins($this->get_joins()); + + // Time modified. + $filters[] = (new filter( + date::class, + 'timemodified', + new lang_string('timemodified', 'core_reportbuilder'), + $this->get_entity_name(), + "{$tagalias}.timemodified" + )) + ->add_joins($this->get_joins()) + ->set_limited_operators([ + date::DATE_ANY, + date::DATE_CURRENT, + date::DATE_LAST, + date::DATE_RANGE, + ]); + + return $filters; + } +} diff --git a/tag/tests/reportbuilder/datasource/tags_test.php b/tag/tests/reportbuilder/datasource/tags_test.php new file mode 100644 index 00000000000..7fe2fcb81ef --- /dev/null +++ b/tag/tests/reportbuilder/datasource/tags_test.php @@ -0,0 +1,261 @@ +. + +declare(strict_types=1); + +namespace core_tag\reportbuilder\datasource; + +use context_course; +use context_user; +use core_collator; +use core_reportbuilder_generator; +use core_reportbuilder_testcase; +use core_reportbuilder\local\filters\{boolean_select, date, select}; +use core_reportbuilder\local\filters\tags as tags_filter; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once("{$CFG->dirroot}/reportbuilder/tests/helpers.php"); + +/** + * Unit tests for tags datasource + * + * @package core_tag + * @covers \core_tag\reportbuilder\datasource\tags + * @copyright 2022 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class tags_test extends core_reportbuilder_testcase { + + /** + * Test default datasource + */ + public function test_datasource_default(): void { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(['tags' => ['Horses']]); + $coursecontext = context_course::instance($course->id); + + $user = $this->getDataGenerator()->create_user(['interests' => ['Pies']]); + $usercontext = context_user::instance($user->id); + + /** @var core_reportbuilder_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); + $report = $generator->create_report(['name' => 'Notes', 'source' => tags::class, 'default' => 1]); + + $content = $this->get_custom_report_content($report->get('id')); + $this->assertCount(2, $content); + + // Consistent order (course, user), just in case. + core_collator::asort_array_of_arrays_by_key($content, 'c3_contextid'); + $content = array_values($content); + + // Default columns are collection, tag name, tag standard, instance context. + [$courserow, $userrow] = array_map('array_values', $content); + + $this->assertEquals('Default collection', $courserow[0]); + $this->assertStringContainsString('Horses', $courserow[1]); + $this->assertEquals('No', $courserow[2]); + $this->assertEquals($coursecontext->get_context_name(), $courserow[3]); + + $this->assertEquals('Default collection', $userrow[0]); + $this->assertStringContainsString('Pies', $userrow[1]); + $this->assertEquals('No', $courserow[2]); + $this->assertEquals($usercontext->get_context_name(), $userrow[3]); + } + + /** + * Test datasource columns that aren't added by default + */ + public function test_datasource_non_default_columns(): void { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(['tags' => ['Horses']]); + $coursecontext = context_course::instance($course->id); + + /** @var core_reportbuilder_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); + $report = $generator->create_report(['name' => 'Notes', 'source' => tags::class, 'default' => 0]); + + // Collection. + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'collection:default']); + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'collection:component']); + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'collection:searchable']); + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'collection:customurl']); + + // Tag. + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'tag:name']); + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'tag:description']); + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'tag:flagged']); + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'tag:timemodified']); + + // Instance. + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'instance:contexturl']); + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'instance:area']); + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'instance:component']); + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'instance:itemtype']); + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'instance:itemid']); + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'instance:timecreated']); + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'instance:timemodified']); + + $content = $this->get_custom_report_content($report->get('id')); + $this->assertCount(1, $content); + + $courserow = array_values($content[0]); + + // Collection. + $this->assertEquals('Yes', $courserow[0]); + $this->assertEmpty($courserow[1]); + $this->assertEquals('Yes', $courserow[2]); + $this->assertEmpty($courserow[3]); + + // Tag. + $this->assertEquals('Horses', $courserow[4]); + $this->assertEmpty($courserow[5]); + $this->assertEquals('No', $courserow[6]); + $this->assertNotEmpty($courserow[7]); + + // Instance. + $this->assertEquals('' . $coursecontext->get_context_name() . '', + $courserow[8]); + $this->assertEquals('Courses', $courserow[9]); + $this->assertEquals('core', $courserow[10]); + $this->assertEquals('course', $courserow[11]); + $this->assertEquals($course->id, $courserow[12]); + $this->assertNotEmpty($courserow[13]); + $this->assertNotEmpty($courserow[14]); + } + + /** + * Data provider for {@see test_datasource_filters} + * + * @return array[] + */ + public function datasource_filters_provider(): array { + return [ + // Collection. + 'Filter collection name' => ['collection:name', [ + 'collection:name_operator' => select::NOT_EQUAL_TO, + 'collection:name_value' => -1, + ], true], + 'Filter collection default' => ['collection:default', [ + 'collection:default_operator' => boolean_select::CHECKED, + ], true], + 'Filter collection default (no match)' => ['collection:default', [ + 'collection:default_operator' => boolean_select::NOT_CHECKED, + ], false], + 'Filter collection searchable' => ['collection:searchable', [ + 'collection:searchable_operator' => boolean_select::CHECKED, + ], true], + 'Filter collection searchable (no match)' => ['collection:searchable', [ + 'collection:searchable_operator' => boolean_select::NOT_CHECKED, + ], false], + + // Tag. + 'Filter tag name' => ['tag:name', [ + 'tag:name_operator' => tags_filter::EQUAL_TO, + 'tag:name_value' => [-1], + ], false], + 'Filter tag name not empty' => ['tag:name', [ + 'tag:name_operator' => tags_filter::NOT_EMPTY, + ], true], + 'Filter tag standard' => ['tag:standard', [ + 'tag:standard_operator' => boolean_select::NOT_CHECKED, + ], true], + 'Filter tag standard (no match)' => ['tag:standard', [ + 'tag:standard_operator' => boolean_select::CHECKED, + ], false], + 'Filter tag flagged' => ['tag:flagged', [ + 'tag:flagged_operator' => boolean_select::NOT_CHECKED, + ], true], + 'Filter tag flagged (no match)' => ['tag:flagged', [ + 'tag:flagged_operator' => boolean_select::CHECKED, + ], false], + 'Filter tag time modified' => ['tag:timemodified', [ + 'tag:timemodified_operator' => date::DATE_RANGE, + 'tag:timemodified_from' => 1622502000, + ], true], + 'Filter tag time modified (no match)' => ['tag:timemodified', [ + 'tag:timemodified_operator' => date::DATE_RANGE, + 'tag:timemodified_to' => 1622502000, + ], false], + + // Instance. + 'Filter instance tag area' => ['instance:area', [ + 'instance:area_operator' => select::EQUAL_TO, + 'instance:area_value' => 'core/course', + ], true], + 'Filter instance tag area (no match)' => ['instance:area', [ + 'instance:area_operator' => select::NOT_EQUAL_TO, + 'instance:area_value' => 'core/course', + ], false], + 'Filter instance time created' => ['instance:timecreated', [ + 'instance:timecreated_operator' => date::DATE_RANGE, + 'instance:timecreated_from' => 1622502000, + ], true], + 'Filter instance time created (no match)' => ['instance:timecreated', [ + 'instance:timecreated_operator' => date::DATE_RANGE, + 'instance:timecreated_to' => 1622502000, + ], false], + 'Filter instance time modified' => ['instance:timemodified', [ + 'instance:timemodified_operator' => date::DATE_RANGE, + 'instance:timemodified_from' => 1622502000, + ], true], + 'Filter instance time modified (no match)' => ['instance:timemodified', [ + 'instance:timemodified_operator' => date::DATE_RANGE, + 'instance:timemodified_to' => 1622502000, + ], false], + ]; + } + + /** + * Test datasource filters + * + * @param string $filtername + * @param array $filtervalues + * @param bool $expectmatch + * + * @dataProvider datasource_filters_provider + */ + public function test_datasource_filters( + string $filtername, + array $filtervalues, + bool $expectmatch + ): void { + $this->resetAfterTest(); + + $this->getDataGenerator()->create_course(['tags' => ['Horses']]); + + /** @var core_reportbuilder_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); + + // Create report containing single tag name, and given filter. + $report = $generator->create_report(['name' => 'Tasks', 'source' => tags::class, 'default' => 0]); + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'tag:name']); + + // Add filter, set it's values. + $generator->create_filter(['reportid' => $report->get('id'), 'uniqueidentifier' => $filtername]); + $content = $this->get_custom_report_content($report->get('id'), 0, $filtervalues); + + if ($expectmatch) { + $this->assertCount(1, $content); + $this->assertEquals('Horses', reset($content[0])); + } else { + $this->assertEmpty($content); + } + } +} diff --git a/user/classes/reportbuilder/datasource/users.php b/user/classes/reportbuilder/datasource/users.php index 92028e6a28b..b632d130303 100644 --- a/user/classes/reportbuilder/datasource/users.php +++ b/user/classes/reportbuilder/datasource/users.php @@ -18,15 +18,17 @@ declare(strict_types=1); namespace core_user\reportbuilder\datasource; +use lang_string; use core_reportbuilder\datasource; use core_reportbuilder\local\entities\user; use core_reportbuilder\local\filters\boolean_select; use core_reportbuilder\local\helpers\database; +use core_tag\reportbuilder\local\entities\tag; /** * Users datasource * - * @package core_reportbuilder + * @package core_user * @copyright 2021 David Matamoros * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -59,9 +61,20 @@ class users extends datasource { $this->add_entity($userentity); + // Join the tag entity. + $tagentity = (new tag()) + ->set_table_alias('tag', $userentity->get_table_alias('tag')) + ->set_entity_title(new lang_string('interests')); + $this->add_entity($tagentity + ->add_joins($userentity->get_tag_joins())); + // Add all columns/filters/conditions from entities to be available in custom reports. - $userentityname = $userentity->get_entity_name(); - $this->add_all_from_entity($userentityname); + $this->add_all_from_entity($userentity->get_entity_name()); + + // Add specific tag entity elements. + $this->add_columns_from_entity($tagentity->get_entity_name(), ['name', 'namewithlink']); + $this->add_filter($tagentity->get_filter('name')); + $this->add_condition($tagentity->get_condition('name')); } /** diff --git a/user/tests/reportbuilder/datasource/users_test.php b/user/tests/reportbuilder/datasource/users_test.php new file mode 100644 index 00000000000..983c77ffb71 --- /dev/null +++ b/user/tests/reportbuilder/datasource/users_test.php @@ -0,0 +1,158 @@ +. + +declare(strict_types=1); + +namespace core_user\reportbuilder\datasource; + +use core_collator; +use core_reportbuilder_testcase; +use core_reportbuilder_generator; +use core_reportbuilder\local\filters\tags; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once("{$CFG->dirroot}/reportbuilder/tests/helpers.php"); + +/** + * Unit tests for users datasources + * + * @package core_user + * @covers \core_user\reportbuilder\datasource\users + * @copyright 2022 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class users_test extends core_reportbuilder_testcase { + + /** + * Test default datasource + */ + public function test_datasource_default(): void { + $this->resetAfterTest(); + + $user = $this->getDataGenerator()->create_user(['email' => 'test@example.com']); + + /** @var core_reportbuilder_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); + $report = $generator->create_report(['name' => 'Users', 'source' => users::class, 'default' => 1]); + + $content = $this->get_custom_report_content($report->get('id')); + $this->assertCount(2, $content); + + // Consistent order by email, just in case. + core_collator::asort_array_of_arrays_by_key($content, 'c2_email'); + $content = array_values($content); + + // Default columns are fullname, username, email. + [$adminrow, $userrow] = array_map('array_values', $content); + + $this->assertEquals(['Admin User', 'admin', 'admin@example.com'], $adminrow); + $this->assertEquals([fullname($user), $user->username, $user->email], $userrow); + } + + /** + * Test datasource columns that aren't added by default + */ + public function test_datasource_non_default_columns(): void { + $this->resetAfterTest(); + + $user = $this->getDataGenerator()->create_user(['fisrtname' => 'Zoe', 'interests' => ['Horses']]); + + /** @var core_reportbuilder_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); + $report = $generator->create_report(['name' => 'Users', 'source' => users::class, 'default' => 0]); + + // User. + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:firstname']); + + // Tags. + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'tag:name']); + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'tag:namewithlink']); + + $content = $this->get_custom_report_content($report->get('id')); + $this->assertCount(2, $content); + + // Consistent order by firstname, just in case. + core_collator::asort_array_of_arrays_by_key($content, 'c0_firstname'); + $content = array_values($content); + + [$adminrow, $userrow] = array_map('array_values', $content); + + $this->assertEquals('Admin', $adminrow[0]); + $this->assertEmpty($adminrow[1]); + $this->assertEmpty($adminrow[2]); + + $this->assertEquals($user->firstname, $userrow[0]); + $this->assertEquals('Horses', $userrow[1]); + $this->assertStringContainsString('Horses', $userrow[2]); + } + + /** + * Data provider for {@see test_datasource_filters} + * + * @return array[] + */ + public function datasource_filters_provider(): array { + return [ + // Tags. + 'Filter tag name' => ['tag:name', [ + 'tag:name_operator' => tags::EQUAL_TO, + 'tag:name_value' => [-1], + ], false], + 'Filter tag name not empty' => ['tag:name', [ + 'tag:name_operator' => tags::NOT_EMPTY, + ], true], + ]; + } + + /** + * Test datasource filters + * + * @param string $filtername + * @param array $filtervalues + * @param bool $expectmatch + * + * @dataProvider datasource_filters_provider + */ + public function test_datasource_filters( + string $filtername, + array $filtervalues, + bool $expectmatch + ): void { + $this->resetAfterTest(); + + $user = $this->getDataGenerator()->create_user(['interests' => ['Horses']]); + + /** @var core_reportbuilder_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); + + // Create report containing single column, and given filter. + $report = $generator->create_report(['name' => 'Tasks', 'source' => users::class, 'default' => 0]); + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:username']); + + // Add filter, set it's values. + $generator->create_filter(['reportid' => $report->get('id'), 'uniqueidentifier' => $filtername]); + $content = $this->get_custom_report_content($report->get('id'), 0, $filtervalues); + + if ($expectmatch) { + $this->assertCount(1, $content); + $this->assertEquals($user->username, reset($content[0])); + } else { + $this->assertEmpty($content); + } + } +}