diff --git a/group/classes/reportbuilder/datasource/groups.php b/group/classes/reportbuilder/datasource/groups.php new file mode 100644 index 00000000000..c9391c31a38 --- /dev/null +++ b/group/classes/reportbuilder/datasource/groups.php @@ -0,0 +1,138 @@ +. + +declare(strict_types=1); + +namespace core_group\reportbuilder\datasource; + +use core_group\reportbuilder\local\entities\{grouping, group, group_member}; +use core_reportbuilder\datasource; +use core_reportbuilder\local\entities\{course, user}; +use core_reportbuilder\local\helpers\database; + +/** + * Groups datasource + * + * @package core_group + * @copyright 2022 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class groups extends datasource { + + /** + * Return user friendly name of the datasource + * + * @return string + */ + public static function get_name(): string { + return get_string('groups', 'core_group'); + } + + /** + * Initialise report + */ + protected function initialise(): void { + $courseentity = new course(); + $coursealias = $courseentity->get_table_alias('course'); + + $this->set_main_table('course', $coursealias); + $this->add_entity($courseentity); + + $paramsiteid = database::generate_param_name(); + $this->add_base_condition_sql("{$coursealias}.id != :{$paramsiteid}", [$paramsiteid => SITEID]); + + // Re-use the context table alias/join from the course entity in subsequent entities. + $contextalias = $courseentity->get_table_alias('context'); + $this->add_join($courseentity->get_context_join()); + + // Group entity. + $groupentity = (new group()) + ->set_table_alias('context', $contextalias); + $groupsalias = $groupentity->get_table_alias('groups'); + $this->add_entity($groupentity + ->add_join("LEFT JOIN {groups} {$groupsalias} ON {$groupsalias}.courseid = {$coursealias}.id")); + + // Grouping entity. + $groupingentity = (new grouping()) + ->set_table_alias('context', $contextalias); + $groupingsalias = $groupingentity->get_table_alias('groupings'); + + // Sub-select for all groupings groups. + $groupinginnerselect = " + SELECT gr.*, grg.groupid + FROM {groupings} gr + JOIN {groupings_groups} grg ON grg.groupingid = gr.id"; + + $this->add_entity($groupingentity + ->add_joins($groupentity->get_joins()) + ->add_join("LEFT JOIN ({$groupinginnerselect}) {$groupingsalias} + ON {$groupingsalias}.courseid = {$coursealias}.id AND {$groupingsalias}.groupid = {$groupsalias}.id")); + + // Group member entity. + $groupmemberentity = new group_member(); + $groupsmembersalias = $groupmemberentity->get_table_alias('groups_members'); + $this->add_entity($groupmemberentity + ->add_joins($groupentity->get_joins()) + ->add_join("LEFT JOIN {groups_members} {$groupsmembersalias} ON {$groupsmembersalias}.groupid = {$groupsalias}.id")); + + // User entity. + $userentity = new user(); + $useralias = $userentity->get_table_alias('user'); + $this->add_entity($userentity + ->add_joins($groupmemberentity->get_joins()) + ->add_join("LEFT JOIN {user} {$useralias} ON {$useralias}.id = {$groupsmembersalias}.userid")); + + // Add all elements from entities to be available in custom reports. + $this->add_all_from_entities(); + } + + /** + * Return the columns that will be added to the report as part of default setup + * + * @return string[] + */ + public function get_default_columns(): array { + return [ + 'course:coursefullnamewithlink', + 'group:name', + 'user:fullname', + ]; + } + + /** + * Return the filters that will be added to the report as part of default setup + * + * @return string[] + */ + public function get_default_filters(): array { + return [ + 'course:fullname', + 'group:name', + ]; + } + + /** + * Return the conditions that will be added to the report as part of default setup + * + * @return string[] + */ + public function get_default_conditions(): array { + return [ + 'course:fullname', + 'group:name', + ]; + } +} diff --git a/group/classes/reportbuilder/local/entities/group.php b/group/classes/reportbuilder/local/entities/group.php new file mode 100644 index 00000000000..9858a97f1ac --- /dev/null +++ b/group/classes/reportbuilder/local/entities/group.php @@ -0,0 +1,263 @@ +. + +declare(strict_types=1); + +namespace core_group\reportbuilder\local\entities; + +use context_course; +use context_helper; +use html_writer; +use lang_string; +use moodle_url; +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}; + +/** + * Group entity + * + * @package core_group + * @copyright 2022 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class group extends base { + + /** + * Database tables that this entity uses and their default aliases + * + * @return array + */ + protected function get_default_table_aliases(): array { + return [ + 'context' => 'gctx', + 'groups' => 'g', + ]; + } + + /** + * The default title for this entity + * + * @return lang_string + */ + protected function get_default_entity_title(): lang_string { + return new lang_string('group', 'core_group'); + } + + /** + * 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; + + $contextalias = $this->get_table_alias('context'); + $groupsalias = $this->get_table_alias('groups'); + + // Name column. + $columns[] = (new column( + 'name', + new lang_string('name'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_TEXT) + ->add_fields("{$groupsalias}.name, {$groupsalias}.courseid") + ->add_fields(context_helper::get_preload_record_columns_sql($contextalias)) + ->set_is_sortable(true) + ->set_callback(static function($name, stdClass $group): string { + if ($name === null) { + return ''; + } + + context_helper::preload_from_record($group); + $context = context_course::instance($group->courseid); + + return format_string($group->name, true, ['context' => $context]); + }); + + // ID number column. + $columns[] = (new column( + 'idnumber', + new lang_string('idnumber'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_TEXT) + ->add_fields("{$groupsalias}.idnumber") + ->set_is_sortable(true); + + // Description column. + $descriptionfieldsql = "{$groupsalias}.description"; + if ($DB->get_dbfamily() === 'oracle') { + $descriptionfieldsql = $DB->sql_order_by_text($descriptionfieldsql, 1024); + } + $columns[] = (new column( + 'description', + new lang_string('description'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_LONGTEXT) + ->add_field($descriptionfieldsql, 'description') + ->add_fields("{$groupsalias}.descriptionformat, {$groupsalias}.id, {$groupsalias}.courseid") + ->add_fields(context_helper::get_preload_record_columns_sql($contextalias)) + ->set_is_sortable(false) + ->set_callback(static function(?string $description, stdClass $group): string { + global $CFG; + + if ($description === null) { + return ''; + } + + require_once("{$CFG->libdir}/filelib.php"); + + context_helper::preload_from_record($group); + $context = context_course::instance($group->courseid); + + $description = file_rewrite_pluginfile_urls($description, 'pluginfile.php', $context->id, 'group', + 'description', $group->id); + + return format_text($description, $group->descriptionformat, ['context' => $context]); + }); + + // Enrolment key column. + $columns[] = (new column( + 'enrolmentkey', + new lang_string('enrolmentkey', 'core_group'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_TEXT) + ->add_fields("{$groupsalias}.enrolmentkey") + ->set_is_sortable(true); + + // Picture column. + $columns[] = (new column( + 'picture', + new lang_string('picture'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_INTEGER) + ->add_fields("{$groupsalias}.picture, {$groupsalias}.id, {$contextalias}.id AS contextid") + ->set_is_sortable(false) + // It doesn't make sense to offer integer aggregation methods for this column. + ->set_disabled_aggregation(['avg', 'max', 'min', 'sum']) + ->set_callback(static function ($picture, stdClass $group): string { + if (empty($group->picture)) { + return ''; + } + + $pictureurl = moodle_url::make_pluginfile_url($group->contextid, 'group', 'icon', $group->id, '/', 'f2'); + $pictureurl->param('rev', $group->picture); + + return html_writer::img($pictureurl, ''); + }); + + // Time created column. + $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("{$groupsalias}.timecreated") + ->set_is_sortable(true) + ->set_callback([format::class, 'userdate']); + + // Time modified column. + $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("{$groupsalias}.timemodified") + ->set_is_sortable(true) + ->set_callback([format::class, 'userdate']); + + return $columns; + } + + /** + * Return list of all available filters + * + * @return filter[] + */ + protected function get_all_filters(): array { + $groupsalias = $this->get_table_alias('groups'); + + // Name filter. + $filters[] = (new filter( + text::class, + 'name', + new lang_string('name'), + $this->get_entity_name(), + "{$groupsalias}.name" + )) + ->add_joins($this->get_joins()); + + // ID number filter. + $filters[] = (new filter( + text::class, + 'idnumber', + new lang_string('idnumber'), + $this->get_entity_name(), + "{$groupsalias}.idnumber" + )) + ->add_joins($this->get_joins()); + + // Time created filter. + $filters[] = (new filter( + date::class, + 'timecreated', + new lang_string('timecreated', 'core_reportbuilder'), + $this->get_entity_name(), + "{$groupsalias}.timecreated" + )) + ->add_joins($this->get_joins()); + + return $filters; + } +} diff --git a/group/classes/reportbuilder/local/entities/group_member.php b/group/classes/reportbuilder/local/entities/group_member.php new file mode 100644 index 00000000000..0139bee11ef --- /dev/null +++ b/group/classes/reportbuilder/local/entities/group_member.php @@ -0,0 +1,132 @@ +. + +declare(strict_types=1); + +namespace core_group\reportbuilder\local\entities; + +use core_reportbuilder\local\filters\date; +use lang_string; +use core_reportbuilder\local\entities\base; +use core_reportbuilder\local\helpers\format; +use core_reportbuilder\local\report\{column, filter}; + +/** + * Group member entity + * + * @package core_group + * @copyright 2022 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class group_member extends base { + + /** + * Database tables that this entity uses and their default aliases + * + * @return array + */ + protected function get_default_table_aliases(): array { + return [ + 'groups_members' => 'gm', + ]; + } + + /** + * The default title for this entity + * + * @return lang_string + */ + protected function get_default_entity_title(): lang_string { + return new lang_string('groupmember', 'core_group'); + } + + /** + * 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 { + $groupsmembersalias = $this->get_table_alias('groups_members'); + + // Time added column. + $columns[] = (new column( + 'timeadded', + new lang_string('timeadded', 'core_reportbuilder'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_TIMESTAMP) + ->add_fields("{$groupsmembersalias}.timeadded") + ->set_is_sortable(true) + ->set_callback([format::class, 'userdate']); + + // Component column. + $columns[] = (new column( + 'component', + new lang_string('plugin', 'core'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_TEXT) + ->add_fields("{$groupsmembersalias}.component") + ->set_is_sortable(true); + + return $columns; + } + + /** + * Return list of all available filters + * + * @return filter[] + */ + protected function get_all_filters(): array { + $groupsmembersalias = $this->get_table_alias('groups_members'); + + // Time added filter. + $filters[] = (new filter( + date::class, + 'timeadded', + new lang_string('timeadded', 'core_reportbuilder'), + $this->get_entity_name(), + "{$groupsmembersalias}.timeadded" + )) + ->add_joins($this->get_joins()); + + return $filters; + } +} diff --git a/group/classes/reportbuilder/local/entities/grouping.php b/group/classes/reportbuilder/local/entities/grouping.php new file mode 100644 index 00000000000..10a4a991944 --- /dev/null +++ b/group/classes/reportbuilder/local/entities/grouping.php @@ -0,0 +1,227 @@ +. + +declare(strict_types=1); + +namespace core_group\reportbuilder\local\entities; + +use context_course; +use context_helper; +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}; + +/** + * Group member entity + * + * @package core_group + * @copyright 2022 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class grouping extends base { + + /** + * Database tables that this entity uses and their default aliases + * + * @return array + */ + protected function get_default_table_aliases(): array { + return [ + 'context' => 'ggctx', + 'groupings' => 'gg', + ]; + } + + /** + * The default title for this entity + * + * @return lang_string + */ + protected function get_default_entity_title(): lang_string { + return new lang_string('grouping', 'core_group'); + } + + /** + * 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; + + $contextalias = $this->get_table_alias('context'); + $groupingsalias = $this->get_table_alias('groupings'); + + // Name column. + $columns[] = (new column( + 'name', + new lang_string('name'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_TEXT) + ->add_fields("{$groupingsalias}.name, {$groupingsalias}.courseid") + ->add_fields(context_helper::get_preload_record_columns_sql($contextalias)) + ->set_is_sortable(true) + ->set_callback(static function($name, stdClass $grouping): string { + if ($name === null) { + return ''; + } + + context_helper::preload_from_record($grouping); + $context = context_course::instance($grouping->courseid); + + return format_string($grouping->name, true, ['context' => $context]); + }); + + // ID number column. + $columns[] = (new column( + 'idnumber', + new lang_string('idnumber'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_TEXT) + ->add_fields("{$groupingsalias}.idnumber") + ->set_is_sortable(true); + + // Description column. + $descriptionfieldsql = "{$groupingsalias}.description"; + if ($DB->get_dbfamily() === 'oracle') { + $descriptionfieldsql = $DB->sql_order_by_text($descriptionfieldsql, 1024); + } + $columns[] = (new column( + 'description', + new lang_string('description'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_LONGTEXT) + ->add_field($descriptionfieldsql, 'description') + ->add_fields("{$groupingsalias}.descriptionformat, {$groupingsalias}.id, {$groupingsalias}.courseid") + ->add_fields(context_helper::get_preload_record_columns_sql($contextalias)) + ->set_is_sortable(false) + ->set_callback(static function(?string $description, stdClass $grouping): string { + global $CFG; + + if ($description === null) { + return ''; + } + + require_once("{$CFG->libdir}/filelib.php"); + + context_helper::preload_from_record($grouping); + $context = context_course::instance($grouping->courseid); + + $description = file_rewrite_pluginfile_urls($description, 'pluginfile.php', $context->id, 'grouping', + 'description', $grouping->id); + + return format_text($description, $grouping->descriptionformat, ['context' => $context]); + }); + + // Time created column. + $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("{$groupingsalias}.timecreated") + ->set_is_sortable(true) + ->set_callback([format::class, 'userdate']); + + // Time modified column. + $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("{$groupingsalias}.timemodified") + ->set_is_sortable(true) + ->set_callback([format::class, 'userdate']); + + return $columns; + } + + /** + * Return list of all available filters + * + * @return filter[] + */ + protected function get_all_filters(): array { + $groupingsalias = $this->get_table_alias('groupings'); + + // Name filter. + $filters[] = (new filter( + text::class, + 'name', + new lang_string('name'), + $this->get_entity_name(), + "{$groupingsalias}.name" + )) + ->add_joins($this->get_joins()); + + // ID number filter. + $filters[] = (new filter( + text::class, + 'idnumber', + new lang_string('idnumber'), + $this->get_entity_name(), + "{$groupingsalias}.idnumber" + )) + ->add_joins($this->get_joins()); + + // Time created filter. + $filters[] = (new filter( + date::class, + 'timecreated', + new lang_string('timecreated', 'core_reportbuilder'), + $this->get_entity_name(), + "{$groupingsalias}.timecreated" + )) + ->add_joins($this->get_joins()); + + return $filters; + } +} diff --git a/group/tests/reportbuilder/datasource/groups_test.php b/group/tests/reportbuilder/datasource/groups_test.php new file mode 100644 index 00000000000..2338babc7d6 --- /dev/null +++ b/group/tests/reportbuilder/datasource/groups_test.php @@ -0,0 +1,355 @@ +. + +declare(strict_types=1); + +namespace core_group\reportbuilder\datasource; + +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 groups datasource + * + * @package core_group + * @covers \core_group\reportbuilder\datasource\groups + * @copyright 2022 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class groups_test extends core_reportbuilder_testcase { + + /** + * Test default datasource + */ + public function test_datasource_default(): void { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $user = $this->getDataGenerator()->create_and_enrol($course, 'student'); + + $group = $this->getDataGenerator()->create_group(['courseid' => $course->id]); + $this->getDataGenerator()->create_group_member(['userid' => $user->id, 'groupid' => $group->id]); + + /** @var core_reportbuilder_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); + $report = $generator->create_report(['name' => 'Groups', 'source' => groups::class, 'default' => 1]); + + $content = $this->get_custom_report_content($report->get('id')); + $this->assertCount(1, $content); + + $contentrow = array_values(reset($content)); + $this->assertEquals([ + "id}\">{$course->fullname}", // Course. + $group->name, // Group. + fullname($user), // User. + ], $contentrow); + } + + /** + * Test datasource groupings reports + */ + public function test_datasource_groupings(): void { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + + // Create group, add to grouping. + $groupone = $this->getDataGenerator()->create_group(['courseid' => $course->id, 'name' => 'Group A']); + $grouping = $this->getDataGenerator()->create_grouping(['courseid' => $course->id]); + $this->getDataGenerator()->create_grouping_group(['groupingid' => $grouping->id, 'groupid' => $groupone->id]); + + // Create second group, no grouping. + $grouptwo = $this->getDataGenerator()->create_group(['courseid' => $course->id, 'name' => 'Group B']); + + /** @var core_reportbuilder_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); + $report = $generator->create_report(['name' => 'Groups', 'source' => groups::class, 'default' => 0]); + + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course:fullname']); + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'grouping:name']); + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'group:name']) + ->set('sortenabled', true)->update(); + + $content = $this->get_custom_report_content($report->get('id')); + $this->assertCount(2, $content); + + $this->assertEquals([ + [ + $course->fullname, + $grouping->name, + $groupone->name, + ], + [ + $course->fullname, + null, + $grouptwo->name, + ], + ], array_map('array_values', $content)); + } + + /** + * Test datasource columns that aren't added by default + */ + public function test_datasource_non_default_columns(): void { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $user = $this->getDataGenerator()->create_and_enrol($course, 'student'); + + $group = $this->getDataGenerator()->create_group(['courseid' => $course->id, 'idnumber' => 'G101', 'enrolmentkey' => 'S', + 'description' => 'My group']); + $this->getDataGenerator()->create_group_member(['userid' => $user->id, 'groupid' => $group->id]); + + $grouping = $this->getDataGenerator()->create_grouping(['courseid' => $course->id, 'idnumber' => 'GR101', + 'description' => 'My grouping']); + $this->getDataGenerator()->create_grouping_group(['groupingid' => $grouping->id, 'groupid' => $group->id]); + + /** @var core_reportbuilder_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); + $report = $generator->create_report(['name' => 'Groups', 'source' => groups::class, 'default' => 0]); + + // Course (just to test join). + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course:shortname']); + + // Group. + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'group:idnumber']); + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'group:description']); + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'group:enrolmentkey']); + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'group:picture']); + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'group:timecreated']); + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'group:timemodified']); + + // Grouping. + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'grouping:name']); + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'grouping:idnumber']); + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'grouping:description']); + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'grouping:timecreated']); + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'grouping:timemodified']); + + // Group member. + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'group_member:timeadded']); + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'group_member:component']); + + // User (just to test join). + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:username']); + + $content = $this->get_custom_report_content($report->get('id')); + $this->assertCount(1, $content); + + [ + $courseshortname, + $groupidnumber, + $groupdescription, + $groupenrolmentkey, + $grouppicture, + $grouptimecreated, + $grouptimemodified, + $groupingname, + $groupingidnumber, + $groupingdescription, + $groupingtimecreated, + $groupingtimemodified, + $groupmembertimeadded, + $groupmemebercomponent, + $userusername, + ] = array_values($content[0]); + + $this->assertEquals($course->shortname, $courseshortname); + $this->assertEquals('G101', $groupidnumber); + $this->assertEquals(format_text($group->description), $groupdescription); + $this->assertEquals('S', $groupenrolmentkey); + $this->assertEmpty($grouppicture); + $this->assertNotEmpty($grouptimecreated); + $this->assertNotEmpty($grouptimemodified); + $this->assertEquals($grouping->name, $groupingname); + $this->assertEquals('GR101', $groupingidnumber); + $this->assertEquals(format_text($grouping->description), $groupingdescription); + $this->assertNotEmpty($groupingtimecreated); + $this->assertNotEmpty($groupingtimemodified); + $this->assertNotEmpty($groupmembertimeadded); + $this->assertEmpty($groupmemebercomponent); + $this->assertEquals($user->username, $userusername); + } + + /** + * Data provider for {@see test_datasource_filters} + * + * @return array[] + */ + public function datasource_filters_provider(): array { + return [ + // Course (just to test join). + 'Filter course name' => ['course:fullname', [ + 'course:fullname_operator' => text::IS_EQUAL_TO, + 'course:fullname_value' => 'Test course', + ], true], + 'Filter course name (no match)' => ['course:fullname', [ + 'course:fullname_operator' => text::IS_NOT_EQUAL_TO, + 'course:fullname_value' => 'Test course', + ], false], + + // Group. + 'Filter group name' => ['group:name', [ + 'group:name_operator' => text::IS_EQUAL_TO, + 'group:name_value' => 'Test group', + ], true], + 'Filter group name (no match)' => ['group:name', [ + 'group:name_operator' => text::IS_NOT_EQUAL_TO, + 'group:name_value' => 'Test group', + ], false], + 'Filter group idnumber' => ['group:idnumber', [ + 'group:idnumber_operator' => text::IS_EQUAL_TO, + 'group:idnumber_value' => 'G101', + ], true], + 'Filter group idnumber (no match)' => ['group:idnumber', [ + 'group:idnumber_operator' => text::IS_NOT_EQUAL_TO, + 'group:idnumber_value' => 'G101', + ], false], + 'Filter group time created' => ['group:timecreated', [ + 'group:timecreated_operator' => date::DATE_RANGE, + 'group:timecreated_from' => 1622502000, + ], true], + 'Filter group time created (no match)' => ['group:timecreated', [ + 'group:timecreated_operator' => date::DATE_RANGE, + 'group:timecreated_to' => 1622502000, + ], false], + + // Grouping. + 'Filter grouping name' => ['grouping:name', [ + 'grouping:name_operator' => text::IS_EQUAL_TO, + 'grouping:name_value' => 'Test grouping', + ], true], + 'Filter grouping name (no match)' => ['grouping:name', [ + 'grouping:name_operator' => text::IS_NOT_EQUAL_TO, + 'grouping:name_value' => 'Test grouping', + ], false], + 'Filter grouping idnumber' => ['grouping:idnumber', [ + 'grouping:idnumber_operator' => text::IS_EQUAL_TO, + 'grouping:idnumber_value' => 'GR101', + ], true], + 'Filter grouping idnumber (no match)' => ['grouping:idnumber', [ + 'grouping:idnumber_operator' => text::IS_NOT_EQUAL_TO, + 'grouping:idnumber_value' => 'GR101', + ], false], + 'Filter grouping time created' => ['grouping:timecreated', [ + 'grouping:timecreated_operator' => date::DATE_RANGE, + 'grouping:timecreated_from' => 1622502000, + ], true], + 'Filter grouping time created (no match)' => ['grouping:timecreated', [ + 'grouping:timecreated_operator' => date::DATE_RANGE, + 'grouping:timecreated_to' => 1622502000, + ], false], + + // Group member. + 'Filter group member time added' => ['group_member:timeadded', [ + 'group_member:timeadded_operator' => date::DATE_RANGE, + 'group_member:timeadded_from' => 1622502000, + ], true], + 'Filter group member time added (no match)' => ['group_member:timeadded', [ + 'group_member:timeadded_operator' => date::DATE_RANGE, + 'group_member:timeadded_to' => 1622502000, + ], false], + + // User (just to test join). + 'Filter user username' => ['user:username', [ + 'user:username_operator' => text::IS_EQUAL_TO, + 'user:username_value' => 'testuser', + ], true], + 'Filter user username (no match)' => ['user:username', [ + 'user:username_operator' => text::IS_NOT_EQUAL_TO, + 'user:username_value' => 'testuser', + ], 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(); + + $course = $this->getDataGenerator()->create_course(['fullname' => 'Test course']); + $user = $this->getDataGenerator()->create_and_enrol($course, 'student', ['username' => 'testuser']); + + $group = $this->getDataGenerator()->create_group(['courseid' => $course->id, 'idnumber' => 'G101', 'name' => 'Test group']); + $this->getDataGenerator()->create_group_member(['userid' => $user->id, 'groupid' => $group->id]); + + $grouping = $this->getDataGenerator()->create_grouping(['courseid' => $course->id, 'idnumber' => 'GR101', + 'name' => 'Test grouping']); + $this->getDataGenerator()->create_grouping_group(['groupingid' => $grouping->id, 'groupid' => $group->id]); + + /** @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' => groups::class, 'default' => 0]); + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'group:idnumber']); + + // 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('G101', 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(); + + $course = $this->getDataGenerator()->create_course(['fullname' => 'Test course']); + $user = $this->getDataGenerator()->create_and_enrol($course, 'student', ['username' => 'testuser']); + + $group = $this->getDataGenerator()->create_group(['courseid' => $course->id, 'idnumber' => 'G101', 'name' => 'Test group']); + $this->getDataGenerator()->create_group_member(['userid' => $user->id, 'groupid' => $group->id]); + + $grouping = $this->getDataGenerator()->create_grouping(['courseid' => $course->id, 'idnumber' => 'GR101', + 'name' => 'Test grouping']); + $this->getDataGenerator()->create_grouping_group(['groupingid' => $grouping->id, 'groupid' => $group->id]); + + $this->datasource_stress_test_columns(groups::class); + $this->datasource_stress_test_columns_aggregation(groups::class); + $this->datasource_stress_test_conditions(groups::class, 'group:idnumber'); + } +} diff --git a/lang/en/moodle.php b/lang/en/moodle.php index c767cc1a21b..380b585ee8e 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -1649,6 +1649,7 @@ $string['phone'] = 'Phone'; $string['phone1'] = 'Phone'; $string['phone2'] = 'Mobile phone'; $string['phpinfo'] = 'PHP info'; +$string['picture'] = 'Picture'; $string['pictureof'] = 'Picture of {$a}'; $string['pictureofuser'] = 'User picture'; $string['placeholdertypeorselect'] = 'Type or select...'; diff --git a/reportbuilder/classes/local/entities/course.php b/reportbuilder/classes/local/entities/course.php index f9a533742a4..c0824d702e7 100644 --- a/reportbuilder/classes/local/entities/course.php +++ b/reportbuilder/classes/local/entities/course.php @@ -116,6 +116,20 @@ class course extends base { return $this; } + /** + * Return syntax for joining on the context table + * + * @return string + */ + public function get_context_join(): string { + $coursealias = $this->get_table_alias('course'); + $contextalias = $this->get_table_alias('context'); + + return "LEFT JOIN {context} {$contextalias} + ON {$contextalias}.contextlevel = " . CONTEXT_COURSE . " + AND {$contextalias}.instanceid = {$coursealias}.id"; + } + /** * Course fields. * @@ -258,11 +272,7 @@ class course extends base { // Join on the context table so that we can use it for formatting these columns later. if ($key === 'coursefullnamewithlink') { - $join = "LEFT JOIN {context} {$contexttablealias} - ON {$contexttablealias}.contextlevel = " . CONTEXT_COURSE . " - AND {$contexttablealias}.instanceid = {$tablealias}.id"; - - $column->add_join($join) + $column->add_join($this->get_context_join()) ->add_fields(context_helper::get_preload_record_columns_sql($contexttablealias)); } @@ -290,11 +300,7 @@ class course extends base { // Join on the context table so that we can use it for formatting these columns later. if ($coursefield === 'summary' || $coursefield === 'shortname' || $coursefield === 'fullname') { - $join = "LEFT JOIN {context} {$contexttablealias} - ON {$contexttablealias}.contextlevel = " . CONTEXT_COURSE . " - AND {$contexttablealias}.instanceid = {$tablealias}.id"; - - $column->add_join($join) + $column->add_join($this->get_context_join()) ->add_field("{$tablealias}.id", 'courseid') ->add_fields(context_helper::get_preload_record_columns_sql($contexttablealias)); }