diff --git a/comment/classes/reportbuilder/datasource/comments.php b/comment/classes/reportbuilder/datasource/comments.php new file mode 100644 index 00000000000..900c4d7d8e3 --- /dev/null +++ b/comment/classes/reportbuilder/datasource/comments.php @@ -0,0 +1,99 @@ +. + +declare(strict_types=1); + +namespace core_comment\reportbuilder\datasource; + +use core_reportbuilder\datasource; +use core_reportbuilder\local\entities\user; +use core_comment\reportbuilder\local\entities\comment; + +/** + * Comments datasource + * + * @package core_comment + * @copyright 2022 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class comments extends datasource { + + /** + * Return user friendly name of the report source + * + * @return string + */ + public static function get_name(): string { + return get_string('comments', 'core_comment'); + } + + /** + * Initialise report + */ + protected function initialise(): void { + $commententity = new comment(); + $commentalias = $commententity->get_table_alias('comments'); + + $this->set_main_table('comments', $commentalias); + + $this->add_entity($commententity); + + // Join the user entity to the comment userid (author). + $userentity = new user(); + $useralias = $userentity->get_table_alias('user'); + $this->add_entity($userentity + ->add_join("LEFT JOIN {user} {$useralias} ON {$useralias}.id = {$commentalias}.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 [ + 'comment:context', + 'comment:content', + 'user:fullname', + 'comment:timecreated', + ]; + } + + /** + * Return the filters that will be added to the report upon creation + * + * @return string[] + */ + public function get_default_filters(): array { + return [ + 'comment:content', + ]; + } + + /** + * Return the conditions that will be added to the report upon creation + * + * @return string[] + */ + public function get_default_conditions(): array { + return [ + 'user:fullname', + ]; + } +} diff --git a/comment/classes/reportbuilder/local/entities/comment.php b/comment/classes/reportbuilder/local/entities/comment.php new file mode 100644 index 00000000000..9a3d92137cd --- /dev/null +++ b/comment/classes/reportbuilder/local/entities/comment.php @@ -0,0 +1,248 @@ +. + +declare(strict_types=1); + +namespace core_comment\reportbuilder\local\entities; + +use context; +use context_helper; +use html_writer; +use lang_string; +use stdClass; +use core_reportbuilder\local\entities\base; +use core_reportbuilder\local\filters\{date, text}; +use core_reportbuilder\local\helpers\format; +use core_reportbuilder\local\report\{column, filter}; + +/** + * Comment entity + * + * @package core_comment + * @copyright 2022 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class comment extends base { + + /** + * Database tables that this entity uses and their default aliases + * + * @return array + */ + protected function get_default_table_aliases(): array { + return [ + 'comments' => 'c', + 'context' => 'cmctx', + ]; + } + + /** + * The default title for this entity + * + * @return lang_string + */ + protected function get_default_entity_title(): lang_string { + return new lang_string('comment', 'core_comment'); + } + + /** + * 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 { + global $DB; + + $commentalias = $this->get_table_alias('comments'); + $contextalias = $this->get_table_alias('context'); + + // Content. + $contentfieldsql = "{$commentalias}.content"; + if ($DB->get_dbfamily() === 'oracle') { + $contentfieldsql = $DB->sql_order_by_text($contentfieldsql, 1024); + } + $columns[] = (new column( + 'content', + new lang_string('content'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_LONGTEXT) + ->add_join("LEFT JOIN {context} {$contextalias} ON {$contextalias}.id = {$commentalias}.contextid") + ->add_field($contentfieldsql, 'content') + ->add_fields("{$commentalias}.format, {$commentalias}.contextid, " . + context_helper::get_preload_record_columns_sql($contextalias)) + ->add_callback(static function($content, stdClass $comment): string { + if ($content === null) { + return ''; + } + + context_helper::preload_from_record($comment); + $context = context::instance_by_id($comment->contextid); + + return format_text($content, $comment->format, ['context' => $context]); + }); + + // 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 = {$commentalias}.contextid") + ->add_fields("{$commentalias}.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 = {$commentalias}.contextid") + ->add_fields("{$commentalias}.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('plugin'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_TEXT) + ->add_fields("{$commentalias}.component"); + + // Area. + $columns[] = (new column( + 'area', + new lang_string('pluginarea'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_TEXT) + ->add_fields("{$commentalias}.commentarea"); + + // Item ID. + $columns[] = (new column( + 'itemid', + new lang_string('pluginitemid'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_INTEGER) + ->add_fields("{$commentalias}.itemid") + ->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("{$commentalias}.timecreated") + ->add_callback([format::class, 'userdate']); + + return $columns; + } + + /** + * Return list of all available filters + * + * @return filter[] + */ + protected function get_all_filters(): array { + global $DB; + + $commentalias = $this->get_table_alias('comments'); + + // Content. + $filters[] = (new filter( + text::class, + 'content', + new lang_string('content'), + $this->get_entity_name(), + $DB->sql_cast_to_char("{$commentalias}.content") + )) + ->add_joins($this->get_joins()); + + // Time created. + $filters[] = (new filter( + date::class, + 'timecreated', + new lang_string('timecreated', 'core_reportbuilder'), + $this->get_entity_name(), + "{$commentalias}.timecreated" + )) + ->add_joins($this->get_joins()) + ->set_limited_operators([ + date::DATE_ANY, + date::DATE_RANGE, + date::DATE_LAST, + date::DATE_CURRENT, + ]); + + return $filters; + } +} diff --git a/comment/tests/reportbuilder/datasource/comments_test.php b/comment/tests/reportbuilder/datasource/comments_test.php new file mode 100644 index 00000000000..08d4aed9241 --- /dev/null +++ b/comment/tests/reportbuilder/datasource/comments_test.php @@ -0,0 +1,231 @@ +. + +declare(strict_types=1); + +namespace core_comment\reportbuilder\datasource; + +use comment; +use context_course; +use core_reportbuilder_generator; +use core_reportbuilder_testcase; +use core_reportbuilder\local\filters\{date, text}; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once("{$CFG->dirroot}/reportbuilder/tests/helpers.php"); + +/** + * Unit tests for comments datasource + * + * @package core_comment + * @covers \core_comment\reportbuilder\datasource\comments + * @copyright 2022 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class comments_test extends core_reportbuilder_testcase { + + /** + * Require test libraries + */ + public static function setUpBeforeClass(): void { + global $CFG; + require_once("{$CFG->dirroot}/comment/lib.php"); + } + + /** + * Test default datasource + */ + public function test_datasource_default(): void { + $this->resetAfterTest(); + $this->setAdminUser(); + + $course = $this->getDataGenerator()->create_course(); + $coursecontext = context_course::instance($course->id); + + $comment = new comment((object) [ + 'context' => $coursecontext, + 'component' => 'block_comments', + 'area' => 'page_comments', + ]); + $comment->add('Cool'); + + /** @var core_reportbuilder_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); + $report = $generator->create_report(['name' => 'Blogs', 'source' => comments::class, 'default' => 1]); + + $content = $this->get_custom_report_content($report->get('id')); + $this->assertCount(1, $content); + + // Default columns are context, content, user, time created. + [$contextname, $content, $userfullname, $timecreated] = array_values($content[0]); + + $this->assertEquals($coursecontext->get_context_name(), $contextname); + $this->assertEquals(format_text('Cool'), $content); + $this->assertEquals(fullname(get_admin()), $userfullname); + $this->assertNotEmpty($timecreated); + } + + /** + * Test datasource columns that aren't added by default + */ + public function test_datasource_non_default_columns(): void { + $this->resetAfterTest(); + $this->setAdminUser(); + + $course = $this->getDataGenerator()->create_course(); + $courseurl = course_get_url($course); + $coursecontext = context_course::instance($course->id); + + $comment = new comment((object) [ + 'context' => $coursecontext, + 'component' => 'block_comments', + 'area' => 'page_comments', + ]); + $comment->add('Cool'); + + /** @var core_reportbuilder_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); + $report = $generator->create_report(['name' => 'Blogs', 'source' => comments::class, 'default' => 0]); + + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'comment:contexturl']); + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'comment:component']); + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'comment:area']); + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'comment:itemid']); + + $content = $this->get_custom_report_content($report->get('id')); + $this->assertCount(1, $content); + + $this->assertEquals([ + "{$coursecontext->get_context_name()}", + 'block_comments', + 'page_comments', + 0, + ], array_values($content[0])); + } + + /** + * Data provider for {@see test_datasource_filters} + * + * @return array[] + */ + public function datasource_filters_provider(): array { + return [ + // Comment. + 'Filter content' => ['comment:content', [ + 'comment:content_operator' => text::CONTAINS, + 'comment:content_value' => 'Cool', + ], true], + 'Filter content (no match)' => ['comment:content', [ + 'comment:content_operator' => text::IS_EQUAL_TO, + 'comment:content_value' => 'Beans', + ], false], + 'Filter time created' => ['comment:timecreated', [ + 'comment:timecreated_operator' => date::DATE_RANGE, + 'comment:timecreated_from' => 1622502000, + ], true], + 'Filter time created (no match)' => ['comment:timecreated', [ + 'comment:timecreated_operator' => date::DATE_RANGE, + 'comment:timecreated_to' => 1622502000, + ], false], + + // User (just to check the join). + 'Filter user' => ['user:username', [ + 'user:username_operator' => text::IS_EQUAL_TO, + 'user:username_value' => 'admin', + ], true], + 'Filter user (no match)' => ['user:username', [ + 'user:username_operator' => text::IS_EQUAL_TO, + 'user:username_value' => 'lionel', + ], 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->setAdminUser(); + + $course = $this->getDataGenerator()->create_course(); + $coursecontext = context_course::instance($course->id); + + $comment = new comment((object) [ + 'context' => $coursecontext, + 'component' => 'block_comments', + 'area' => 'page_comments', + ]); + $comment->add('Cool'); + + /** @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' => comments::class, 'default' => 0]); + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'comment:component']); + + // 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('block_comments', reset($content[0])); + } else { + $this->assertEmpty($content); + } + } + + /** + * Stress test datasource + * + * In order to execute this test PHPUNIT_LONGTEST should be defined as true in phpunit.xml or directly in config.php + */ + public function test_stress_datasource(): void { + if (!PHPUNIT_LONGTEST) { + $this->markTestSkipped('PHPUNIT_LONGTEST is not defined'); + } + + $this->resetAfterTest(); + $this->setAdminUser(); + + $course = $this->getDataGenerator()->create_course(); + $coursecontext = context_course::instance($course->id); + + $comment = new comment((object) [ + 'context' => $coursecontext, + 'component' => 'block_comments', + 'area' => 'page_comments', + ]); + $comment->add('Cool'); + + $this->datasource_stress_test_columns(comments::class); + $this->datasource_stress_test_columns_aggregation(comments::class); + $this->datasource_stress_test_conditions(comments::class, 'comment:component'); + } +} diff --git a/lang/en/comment.php b/lang/en/comment.php index 2d1bdf4b43f..0af21f7a134 100644 --- a/lang/en/comment.php +++ b/lang/en/comment.php @@ -22,6 +22,8 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +$string['comment'] = 'Comment'; +$string['comments'] = 'Comments'; $string['commentsubcontext'] = 'Comments'; $string['privacy:metadata:comment'] = 'Stores comments of users.'; $string['privacy:metadata:comment:content'] = 'Stores the text of the comment.'; diff --git a/lang/en/moodle.php b/lang/en/moodle.php index c767cc1a21b..c36f447ff46 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -1656,8 +1656,10 @@ $string['pleaseclose'] = 'Please close this window now.'; $string['pleasesearchmore'] = 'Please search some more'; $string['pleaseusesearch'] = 'Please use the search'; $string['plugin'] = 'Plugin'; +$string['pluginarea'] = 'Area'; $string['plugindeletefiles'] = 'All data associated with the plugin \'{$a->name}\' has been deleted from the database. To prevent the plugin re-installing itself, you should now delete this directory from your server: {$a->directory}'; $string['plugincheck'] = 'Plugins check'; +$string['pluginitemid'] = 'Item ID'; $string['pluginsetup'] = 'Setting up plugin tables'; $string['policyaccept'] = 'I understand and agree'; $string['policyagree'] = 'You must agree to this policy to continue using this site. Do you agree?';