MDL-72172 cohort: implement cohort datasource for custom reporting.

Create two entities exposing reportable data on site cohorts and
their members, via column and filter definitions.

Create report source bringing them together along with the user
entity to provide data for the reportbuilder editor.

Co-authored-by: Carlos Castillo <carlos.castillo@moodle.com>
This commit is contained in:
Paul Holden 2021-07-20 17:38:52 +01:00 committed by David Matamoros
parent 2b2897bf10
commit bbf95413fb
6 changed files with 695 additions and 0 deletions

View File

@ -0,0 +1,280 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
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 <paulh@moodle.com>
* @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;
}
}

View File

@ -0,0 +1,120 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
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 <paulh@moodle.com>
* @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;
}
}

View File

@ -0,0 +1,120 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
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 <paulh@moodle.com>
* @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 [];
}
}

View File

@ -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"

View File

@ -0,0 +1,74 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
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 <paulh@moodle.com>
* @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.
'<div class="text_to_html">Cohort for the legends</div>', // Description.
'Lionel Richards', // User.
], $contentrow);
}
}

View File

@ -33,6 +33,7 @@ $string['bulkadd'] = 'Add to cohort';
$string['bulknocohort'] = 'No available cohorts found';
$string['categorynotfound'] = 'Category <b>{$a}</b> 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';