diff --git a/cohort/classes/local/entities/cohort.php b/cohort/classes/local/entities/cohort.php new file mode 100644 index 00000000000..992a9761eb2 --- /dev/null +++ b/cohort/classes/local/entities/cohort.php @@ -0,0 +1,280 @@ +. + +declare(strict_types=1); + +namespace core_cohort\local\entities; + +use context; +use context_helper; +use lang_string; +use stdClass; +use core_reportbuilder\local\entities\base; +use core_reportbuilder\local\filters\date; +use core_reportbuilder\local\filters\select; +use core_reportbuilder\local\filters\text; +use core_reportbuilder\local\helpers\format; +use core_reportbuilder\local\report\column; +use core_reportbuilder\local\report\filter; + +/** + * Cohort entity + * + * @package core_cohort + * @copyright 2021 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class cohort extends base { + + /** + * Database tables that this entity uses and their default aliases + * + * @return array + */ + protected function get_default_table_aliases(): array { + return ['cohort' => 'c']; + } + + /** + * The default title for this entity + * + * @return lang_string + */ + protected function get_default_entity_title(): lang_string { + return new lang_string('cohort', 'core_cohort'); + } + + /** + * 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 { + $tablealias = $this->get_table_alias('cohort'); + + // Category/context column. + $columns[] = (new column( + 'context', + new lang_string('category'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_INTEGER) + ->add_fields("{$tablealias}.contextid") + ->set_is_sortable(true) + ->add_callback(static function(int $contextid): string { + return context::instance_by_id($contextid)->get_context_name(false); + }); + + // Name column. + $columns[] = (new column( + 'name', + new lang_string('name', 'core_cohort'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_TEXT) + ->add_fields("{$tablealias}.name") + ->set_is_sortable(true); + + // ID number column. + $columns[] = (new column( + 'idnumber', + new lang_string('idnumber', 'core_cohort'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_TEXT) + ->add_fields("{$tablealias}.idnumber") + ->set_is_sortable(true); + + // Description column. + $columns[] = (new column( + 'description', + new lang_string('description'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_TEXT) + ->add_fields("{$tablealias}.description, {$tablealias}.descriptionformat, {$tablealias}.id, {$tablealias}.contextid") + ->add_callback(static function(string $description, stdClass $cohort): string { + global $CFG; + require_once("{$CFG->libdir}/filelib.php"); + + $description = file_rewrite_pluginfile_urls($description, 'pluginfile.php', $cohort->contextid, 'cohort', + 'description', $cohort->id); + + return format_text($description, $cohort->descriptionformat, ['context' => $cohort->contextid]); + }) + ->set_is_sortable(false); + + // Visible column. + $columns[] = (new column( + 'visible', + new lang_string('visible', 'core_cohort'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_BOOLEAN) + ->add_fields("{$tablealias}.visible") + ->set_is_sortable(true) + ->set_callback([format::class, 'boolean_as_text']); + + // 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("{$tablealias}.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("{$tablealias}.timemodified") + ->set_is_sortable(true) + ->set_callback([format::class, 'userdate']); + + // Component column. + $columns[] = (new column( + 'component', + new lang_string('component', 'core_cohort'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_TEXT) + ->add_fields("{$tablealias}.component") + ->set_is_sortable(true) + ->add_callback(static function(string $component): string { + return empty($component) + ? get_string('nocomponent', 'cohort') + : get_string('pluginname', $component); + }); + + // Theme column. + $columns[] = (new column( + 'theme', + new lang_string('theme'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_TEXT) + ->add_fields("{$tablealias}.theme") + ->set_is_sortable(true); + + return $columns; + } + + /** + * Return list of all available filters + * + * @return filter[] + */ + protected function get_all_filters(): array { + $tablealias = $this->get_table_alias('cohort'); + + // Context filter. + $filters[] = (new filter( + select::class, + 'context', + new lang_string('category'), + $this->get_entity_name(), + "{$tablealias}.contextid" + )) + ->add_joins($this->get_joins()) + ->set_options_callback(static function(): array { + global $DB; + + // Load all contexts in which there are cohorts. + $ctxfields = context_helper::get_preload_record_columns_sql('ctx'); + $contexts = $DB->get_records_sql(" + SELECT DISTINCT {$ctxfields}, c.contextid + FROM {context} ctx + JOIN {cohort} c ON c.contextid = ctx.id"); + + // Transform context record into it's name (used as the filter options). + return array_map(static function(stdClass $contextrecord): string { + context_helper::preload_from_record($contextrecord); + + return context::instance_by_id($contextrecord->contextid) + ->get_context_name(false); + }, $contexts); + }); + + // Name filter. + $filters[] = (new filter( + text::class, + 'name', + new lang_string('name', 'core_cohort'), + $this->get_entity_name(), + "{$tablealias}.name" + )) + ->add_joins($this->get_joins()); + + // ID number filter. + $filters[] = (new filter( + text::class, + 'idnumber', + new lang_string('idnumber', 'core_cohort'), + $this->get_entity_name(), + "{$tablealias}.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(), + "{$tablealias}.timecreated" + )) + ->add_joins($this->get_joins()); + + return $filters; + } +} diff --git a/cohort/classes/local/entities/cohort_member.php b/cohort/classes/local/entities/cohort_member.php new file mode 100644 index 00000000000..b05a661714b --- /dev/null +++ b/cohort/classes/local/entities/cohort_member.php @@ -0,0 +1,120 @@ +. + +declare(strict_types=1); + +namespace core_cohort\local\entities; + +use lang_string; +use core_reportbuilder\local\entities\base; +use core_reportbuilder\local\filters\date; +use core_reportbuilder\local\helpers\format; +use core_reportbuilder\local\report\column; +use core_reportbuilder\local\report\filter; + +/** + * Cohort member entity + * + * @package core_cohort + * @copyright 2021 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class cohort_member extends base { + + /** + * Database tables that this entity uses and their default aliases + * + * @return array + */ + protected function get_default_table_aliases(): array { + return ['cohort_members' => 'cm']; + } + + /** + * The default title for this entity + * + * @return lang_string + */ + protected function get_default_entity_title(): lang_string { + return new lang_string('cohortmember', 'core_cohort'); + } + + /** + * 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 { + $tablealias = $this->get_table_alias('cohort_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("{$tablealias}.timeadded") + ->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 { + $tablealias = $this->get_table_alias('cohort_members'); + + // Time added filter. + $filters[] = (new filter( + date::class, + 'timeadded', + new lang_string('timeadded', 'core_reportbuilder'), + $this->get_entity_name(), + "{$tablealias}.timeadded" + )) + ->add_joins($this->get_joins()); + + return $filters; + } +} diff --git a/cohort/classes/reportbuilder/datasource/cohorts.php b/cohort/classes/reportbuilder/datasource/cohorts.php new file mode 100644 index 00000000000..4f63e884a16 --- /dev/null +++ b/cohort/classes/reportbuilder/datasource/cohorts.php @@ -0,0 +1,120 @@ +. + +declare(strict_types=1); + +namespace core_cohort\reportbuilder\datasource; + +use core_cohort\local\entities\cohort; +use core_cohort\local\entities\cohort_member; +use core_reportbuilder\datasource; +use core_reportbuilder\local\entities\user; + +/** + * Cohorts datasource + * + * @package core_cohort + * @copyright 2021 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class cohorts extends datasource { + + /** + * Return user friendly name of the datasource + * + * @return string + */ + public static function get_name(): string { + return get_string('cohorts', 'core_cohort'); + } + + /** + * Initialise report + */ + protected function initialise(): void { + $cohortentity = new cohort(); + $cohorttablealias = $cohortentity->get_table_alias('cohort'); + + $this->set_main_table('cohort', $cohorttablealias); + + $this->add_entity($cohortentity); + + // Join the cohort member entity to the cohort entity. + $cohortmemberentity = new cohort_member(); + $cohortmembertablealias = $cohortmemberentity->get_table_alias('cohort_members'); + + $cohortmemberjoin = "LEFT JOIN {cohort_members} {$cohortmembertablealias} + ON {$cohortmembertablealias}.cohortid = {$cohorttablealias}.id"; + + $this->add_entity($cohortmemberentity->add_join($cohortmemberjoin)); + + // Join the user entity to the cohort member entity. + $userentity = new user(); + $usertablealias = $userentity->get_table_alias('user'); + + $userjoin = "LEFT JOIN {user} {$usertablealias} + ON {$usertablealias}.id = {$cohortmembertablealias}.userid"; + + $this->add_entity($userentity->add_joins([$cohortmemberjoin, $userjoin])); + + // Add all columns from entities to be available in custom reports. + $this->add_columns_from_entity($cohortentity->get_entity_name()); + $this->add_columns_from_entity($cohortmemberentity->get_entity_name()); + $this->add_columns_from_entity($userentity->get_entity_name()); + + // Add all filters from entities to be available in custom reports. + $this->add_filters_from_entity($cohortentity->get_entity_name()); + $this->add_filters_from_entity($cohortmemberentity->get_entity_name()); + $this->add_filters_from_entity($userentity->get_entity_name()); + + // Add all conditions from entities to be available in custom reports. + $this->add_conditions_from_entity($cohortentity->get_entity_name()); + $this->add_conditions_from_entity($cohortmemberentity->get_entity_name()); + $this->add_conditions_from_entity($userentity->get_entity_name()); + } + + /** + * Return the columns that will be added to the report as part of default setup + * + * @return string[] + */ + public function get_default_columns(): array { + return [ + 'cohort:context', + 'cohort:name', + 'cohort:idnumber', + 'cohort:description', + ]; + } + + /** + * Return the filters that will be added to the report once is created + * + * @return string[] + */ + public function get_default_filters(): array { + return ['cohort:context', 'cohort:name']; + } + + /** + * Return the conditions that will be added to the report once is created + * + * @return string[] + */ + public function get_default_conditions(): array { + return []; + } +} diff --git a/cohort/tests/behat/reportbuilder.feature b/cohort/tests/behat/reportbuilder.feature new file mode 100644 index 00000000000..cc693ffb364 --- /dev/null +++ b/cohort/tests/behat/reportbuilder.feature @@ -0,0 +1,100 @@ +@core_reportbuilder @javascript +Feature: Manage custom reports for cohorts + In order to manage custom reports for cohorts + As an admin and user + I need to create new, view and edit existing reports + + Background: + Given the following "cohorts" exist: + | name | idnumber | contextid | + | Another one | AO | 1 | + | MDL-62161 | 62161 | 1 | + | New system cohort | NSC | 1 | + | MDL-62162 | 62162 | 1 | + | Other cohort | LC | 3 | + And the following "users" exist: + | username | firstname | lastname | email | + | user1 | Alice | Last1 | user1@example.com | + | user2 | Carlos | Last2 | user2@example.com | + | user3 | Paul | Last3 | user3@example.com | + | user4 | Juan | Last4 | user4@example.com | + | user5 | Pedro | Last5 | user5@example.com | + | user6 | Luis | Last6 | user6@example.com | + | user7 | David | Last7 | user7@example.com | + | user8 | Zoe | Last8 | user8@example.com | + And the following "cohort members" exist: + | user | cohort | + | user1 | AO | + | user2 | AO | + | user3 | AO | + | user4 | AO | + | user5 | 62161 | + | user6 | 62161 | + | user7 | NSC | + | user8 | NSC | + And the following "core_reportbuilder > Reports" exist: + | name | source | default | + | My report | core_cohort\reportbuilder\datasource\cohorts | 0 | + And the following "core_reportbuilder > Columns" exist: + | report | uniqueidentifier | + | My report | cohort:context | + | My report | cohort:name | + + Scenario: Add condition to cohorts report + Given I am on the "My report" "reportbuilder > Editor" page logged in as "admin" + And I change window size to "large" + When I click on "Show/hide settings sidebar" "button" + And I click on "Show/hide 'Conditions'" "button" + Then I should see "There are no conditions selected" in the "[data-region='settings-conditions']" "css_element" + And I set the field "Select a condition" to "Category" + And I should see "Added condition 'Category'" + And I should not see "There are no conditions selected" in the "[data-region='settings-conditions']" "css_element" + And I set the following fields in the "Category" "core_reportbuilder > Condition" to these values: + | Category operator | Is equal to | + | Category value | 3 | + And I click on "Apply" "button" in the "[data-region='settings-conditions']" "css_element" + And I should see "Conditions applied" + And I should see "Other cohort" in the "reportbuilder-table" "table" + And I should not see "MDL-62162" in the "reportbuilder-table" "table" + + Scenario: Use filters in cohorts report + Given I am on the "My report" "reportbuilder > Editor" page logged in as "admin" + And I change window size to "large" + When I click on "Show/hide settings sidebar" "button" + And I click on "Show/hide 'Filters'" "button" + Then I should see "There are no filters selected" in the "[data-region='settings-filters']" "css_element" + And I set the field "Select a filter" to "Name" + And I should see "Other cohort" in the ".reportbuilder-table" "css_element" + And I should see "MDL-62162" in the ".reportbuilder-table" "css_element" + When I click on "Switch to preview mode" "button" + And I click on "Filters" "button" in the "[data-region='core_reportbuilder/report-header']" "css_element" + And I set the following fields in the "Name" "core_reportbuilder > Filter" to these values: + | Name operator | Contains | + | Name value | Another | + And I click on "Apply" "button" in the "[data-region='core_reportbuilder/report-header']" "css_element" + Then the following should exist in the "reportbuilder-table" table: + | Category | Name | + | System | Another one | + And the following should not exist in the "reportbuilder-table" table: + | Category | Name | + | Miscellaneous | Other cohort | + + Scenario: Use sorting and aggregations in cohorts report + Given the following "core_reportbuilder > Columns" exist: + | report | uniqueidentifier | + | My report | user:lastname | + And I am on the "My report" "reportbuilder > Editor" page logged in as "admin" + And I set the field "Rename column 'Surname'" to "Members" + And I reload the page + And I set the field "Aggregate column 'Surname'" to "Comma separated distinct values" + And I click on "Show/hide settings sidebar" "button" + And I click on "Show/hide 'Sorting'" "button" + And I change window size to "large" + And I click on "Move sorting for column 'Surname'" "button" + And I click on "To the top of the list" "link" in the "Move sorting for column 'Surname'" "dialogue" + And I click on "Enable sorting for column 'Surname'" "checkbox" + And "Another one" "table_row" should appear before "MDL-62161" "table_row" + When I click on "Sort column 'Surname' descending" "button" + Then I should see "Updated sorting for column 'Surname'" + And I wait "1" seconds + And "MDL-62161" "table_row" should appear before "Another one" "table_row" diff --git a/cohort/tests/datasource_test.php b/cohort/tests/datasource_test.php new file mode 100644 index 00000000000..eb6533d533c --- /dev/null +++ b/cohort/tests/datasource_test.php @@ -0,0 +1,74 @@ +. + +declare(strict_types=1); + +namespace core_cohort\reportbuilder\datasource; + +use core_reportbuilder_testcase; +use core_reportbuilder_generator; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once("{$CFG->dirroot}/reportbuilder/tests/helpers.php"); + +/** + * Unit tests for cohorts datasource + * + * @package core_cohort + * @covers \core_cohort\reportbuilder\datasource\cohorts + * @copyright 2021 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class datasource_test extends core_reportbuilder_testcase { + + /** + * Test cohorts datasource + */ + public function test_cohorts_datasource(): void { + $this->resetAfterTest(); + + // Test subject. + $cohort = $this->getDataGenerator()->create_cohort([ + 'name' => 'Legends', + 'idnumber' => 'C101', + 'description' => 'Cohort for the legends', + ]); + + $user = $this->getDataGenerator()->create_user(['firstname' => 'Lionel', 'lastname' => 'Richards']); + cohort_add_member($cohort->id, $user->id); + + /** @var core_reportbuilder_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); + $report = $generator->create_report(['name' => 'Cohorts', 'source' => cohorts::class]); + + // Add user fullname column to the report. + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:fullname']); + + $content = $this->get_custom_report_content($report->get('id')); + $this->assertCount(1, $content); + + $contentrow = array_values(reset($content)); + $this->assertEquals([ + 'System', // Context. + 'Legends', // Name. + 'C101', // ID number. + '
Cohort for the legends
', // Description. + 'Lionel Richards', // User. + ], $contentrow); + } +} diff --git a/lang/en/cohort.php b/lang/en/cohort.php index 9badc3818aa..9396d8c8aa6 100644 --- a/lang/en/cohort.php +++ b/lang/en/cohort.php @@ -33,6 +33,7 @@ $string['bulkadd'] = 'Add to cohort'; $string['bulknocohort'] = 'No available cohorts found'; $string['categorynotfound'] = 'Category {$a} not found or you don\'t have permission to create a cohort there. The default context will be used.'; $string['cohort'] = 'Cohort'; +$string['cohortmember'] = 'Cohort member'; $string['cohorts'] = 'Cohorts'; $string['cohortsin'] = '{$a}: available cohorts'; $string['assigncohorts'] = 'Assign cohort members';