From 065b38f68d91187a0448a2980d0dea8488fca0bf Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Mon, 18 Jul 2022 16:49:30 +0100 Subject: [PATCH] MDL-75245 tag: implement tags datasource for custom reporting. Create entity definitions for collections, tags and instances for new report source to provide data for the reportbuilder editor. --- lang/en/moodle.php | 2 + lang/en/tag.php | 6 + tag/classes/reportbuilder/datasource/tags.php | 119 ++++++++ .../local/entities/collection.php | 198 ++++++++++++ .../reportbuilder/local/entities/instance.php | 283 ++++++++++++++++++ .../reportbuilder/local/entities/tag.php | 242 +++++++++++++++ .../reportbuilder/datasource/tags_test.php | 258 ++++++++++++++++ 7 files changed, 1108 insertions(+) create mode 100644 tag/classes/reportbuilder/datasource/tags.php create mode 100644 tag/classes/reportbuilder/local/entities/collection.php create mode 100644 tag/classes/reportbuilder/local/entities/instance.php create mode 100644 tag/classes/reportbuilder/local/entities/tag.php create mode 100644 tag/tests/reportbuilder/datasource/tags_test.php diff --git a/lang/en/moodle.php b/lang/en/moodle.php index bd3e25265e8e0..97e7d7e4a7797 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -293,6 +293,8 @@ $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 4507a97db0635..441e4a217f66a 100644 --- a/lang/en/tag.php +++ b/lang/en/tag.php @@ -75,7 +75,9 @@ $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['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['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['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/tag/classes/reportbuilder/datasource/tags.php b/tag/classes/reportbuilder/datasource/tags.php new file mode 100644 index 0000000000000..18b693bec23df --- /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 0000000000000..05c8f81ea7eb4 --- /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 0000000000000..818d51554d627 --- /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 0000000000000..e938de5e4a47f --- /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, text}; +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( + text::class, + 'name', + new lang_string('name', 'core_tag'), + $this->get_entity_name(), + "{$tagalias}.rawname" + )) + ->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 0000000000000..4aa26a75733f1 --- /dev/null +++ b/tag/tests/reportbuilder/datasource/tags_test.php @@ -0,0 +1,258 @@ +. + +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, text}; + +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' => text::IS_EQUAL_TO, + 'tag:name_value' => 'Horses', + ], 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); + } + } +}