From c25882e8df9b387957a89e5a8cd40530628d0cb7 Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Wed, 8 Nov 2023 12:46:35 +0000 Subject: [PATCH] MDL-79270 admin: new filter type for filtering users by course role. Use in the new browse users system report, to which it is quite specific. The filter itself is a like for like replacement of the pre-existing filter on the same page, and is really a combination of existing role/category/course filters. --- .../local/filters/courserole.php | 107 +++++++++++++++++ .../local/systemreports/users.php | 13 +- .../local/filters/courserole_test.php | 113 ++++++++++++++++++ 3 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 admin/classes/reportbuilder/local/filters/courserole.php create mode 100644 admin/tests/reportbuilder/local/filters/courserole_test.php diff --git a/admin/classes/reportbuilder/local/filters/courserole.php b/admin/classes/reportbuilder/local/filters/courserole.php new file mode 100644 index 00000000000..162a1aeff21 --- /dev/null +++ b/admin/classes/reportbuilder/local/filters/courserole.php @@ -0,0 +1,107 @@ +. + +declare(strict_types=1); + +namespace core_admin\reportbuilder\local\filters; + +use core\context\system; +use core_course_category; +use MoodleQuickForm; +use core_reportbuilder\local\filters\base; +use core_reportbuilder\local\helpers\database; + +/** + * Course role report filter (by role, category, course) + * + * The provided filter field SQL must refer/return an expression for the user ID (e.g. "{$user}.id") + * + * @package core_admin + * @copyright 2023 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class courserole extends base { + + /** + * Setup form + * + * @param MoodleQuickForm $mform + */ + public function setup_form(MoodleQuickForm $mform): void { + $elements = []; + + // Role. + $elements['role'] = $mform->createElement('select', "{$this->name}_role", get_string('rolefullname', 'core_role'), + [0 => get_string('anyrole', 'core_filters')] + get_default_enrol_roles(system::instance())); + + // Category. + $elements['category'] = $mform->createElement('select', "{$this->name}_category", get_string('category'), + [0 => get_string('anycategory', 'core_filters')] + core_course_category::make_categories_list()); + + // Course. + $elements['course'] = $mform->createElement('text', "{$this->name}_course", get_string('shortnamecourse')); + $mform->setType("{$this->name}_course", PARAM_RAW); + + $mform->addElement('group', "{$this->name}_group", '', $elements, '', false); + } + + /** + * Return filter SQL + * + * @param array $values + * @return array + */ + public function get_sql_filter(array $values): array { + [$fieldsql, $params] = $this->filter->get_field_sql_and_params(); + + [$contextalias, $rolealias, $coursealias] = database::generate_aliases(3); + [$roleparam, $categoryparam, $courseparam] = database::generate_param_names(3); + + // Role. + $role = (int) ($values["{$this->name}_role"] ?? 0); + if ($role > 0) { + $selects[] = "{$rolealias}.roleid = :{$roleparam}"; + $params[$roleparam] = $role; + } + + // Category. + $category = (int) ($values["{$this->name}_category"] ?? 0); + if ($category > 0) { + $selects[] = "{$coursealias}.category = :{$categoryparam}"; + $params[$categoryparam] = $category; + } + + // Course. + $course = $values["{$this->name}_course"] ?? ''; + if ($course !== '') { + $selects[] = "{$coursealias}.shortname = :{$courseparam}"; + $params[$courseparam] = $course; + } + + // Filter values are not set. + if (empty($selects)) { + return ['', []]; + } + + return ["{$fieldsql} IN ( + SELECT {$rolealias}.userid + FROM {role_assignments} {$rolealias} + JOIN {context} {$contextalias} ON {$contextalias}.id = {$rolealias}.contextid AND {$contextalias}.contextlevel = 50 + JOIN {course} {$coursealias} ON {$coursealias}.id = {$contextalias}.instanceid + WHERE " . implode(' AND ', $selects) . " + )", $params]; + } +} diff --git a/admin/classes/reportbuilder/local/systemreports/users.php b/admin/classes/reportbuilder/local/systemreports/users.php index db8a67a1d97..df6616079ed 100644 --- a/admin/classes/reportbuilder/local/systemreports/users.php +++ b/admin/classes/reportbuilder/local/systemreports/users.php @@ -16,6 +16,7 @@ namespace core_admin\reportbuilder\local\systemreports; +use core_admin\reportbuilder\local\filters\courserole; use core\context\system; use core_cohort\reportbuilder\local\entities\cohort; use core_cohort\reportbuilder\local\entities\cohort_member; @@ -218,7 +219,17 @@ class users extends system_report { $now1 => $now, $now2 => $now, ]) - ->add_joins($this->get_joins())); + ); + + // Course role filter. + $this->add_filter((new filter( + courserole::class, + 'courserole', + new lang_string('courserole', 'filters'), + $this->get_entity('user')->get_entity_name(), + )) + ->set_field_sql("{$entityuseralias}.id") + ); // Add user profile fields filters. $userprofilefields = new user_profile_fields($entityuseralias . '.id', $entityuser->get_entity_name()); diff --git a/admin/tests/reportbuilder/local/filters/courserole_test.php b/admin/tests/reportbuilder/local/filters/courserole_test.php new file mode 100644 index 00000000000..0e721bfe356 --- /dev/null +++ b/admin/tests/reportbuilder/local/filters/courserole_test.php @@ -0,0 +1,113 @@ +. + +declare(strict_types=1); + +namespace core_admin\reportbuilder\local\filters; + +use advanced_testcase; +use lang_string; +use core_reportbuilder\local\report\filter; + +/** + * Unit tests for course role report filter + * + * @package core_admin + * @covers \core_admin\reportbuilder\local\filters\courserole + * @copyright 2023 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class courserole_test extends advanced_testcase { + + /** + * Data provider for {@see test_get_sql_filter} + * + * @return array[] + */ + public static function get_sql_filter_provider(): array { + return [ + 'Empty filter' => ['', '', '', ['admin', 'guest', 'user1', 'user2', 'user3', 'user4']], + 'Filter by role' => ['editingteacher', '', '', ['user1', 'user3', 'user4']], + 'Filter by role in category' => ['editingteacher', 'cat2', '', ['user1', 'user3']], + 'Filter by role in category and course' => ['editingteacher', 'cat2', 'course2', ['user1']], + 'Filter by role in course' => ['student', '', 'course2', ['user2']], + 'Filter by category' => ['', 'cat2', '', ['user1', 'user2', 'user3']], + 'Filter by category and course' => ['', 'cat2', 'course2', ['user1', 'user2']], + 'Filter by course' => ['', '', 'course3', ['user3']], + ]; + } + + /** + * Test getting filter SQL + * + * @param string $rolename + * @param string $categoryname + * @param string $course + * @param string[] $expectedusers + * + * @dataProvider get_sql_filter_provider + */ + public function test_get_sql_filter( + string $rolename, + string $categoryname, + string $course, + array $expectedusers, + ): void { + global $DB; + + $this->resetAfterTest(); + + // Create categories/course structure (categoryone: courseone; categorytwo: coursetwo/coursethree). + $categoryone = $this->getDataGenerator()->create_category(['name' => 'cat1']); + $courseone = $this->getDataGenerator()->create_course(['category' => $categoryone->id, 'shortname' => 'course1']); + + $categorytwo = $this->getDataGenerator()->create_category(['name' => 'cat2']); + $coursetwo = $this->getDataGenerator()->create_course(['category' => $categorytwo->id, 'shortname' => 'course2']); + $coursethree = $this->getDataGenerator()->create_course(['category' => $categorytwo->id, 'shortname' => 'course3']); + + // User one is a teacher in courseone/coursetwo. + $userone = $this->getDataGenerator()->create_and_enrol($courseone, 'editingteacher', ['username' => 'user1']); + $this->getDataGenerator()->enrol_user($userone->id, $coursetwo->id, 'editingteacher'); + + // User two is a student in coursetwo. + $usertwo = $this->getDataGenerator()->create_and_enrol($coursetwo, 'student', ['username' => 'user2']); + + // User three is a teacher in courseone/coursethree. + $userthree = $this->getDataGenerator()->create_and_enrol($courseone, 'editingteacher', ['username' => 'user3']); + $this->getDataGenerator()->enrol_user($userthree->id, $coursethree->id, 'editingteacher'); + + // User four is a teacher in courseone. + $userfour = $this->getDataGenerator()->create_and_enrol($courseone, 'editingteacher', ['username' => 'user4']); + + $filter = new filter( + courserole::class, + 'test', + new lang_string('yes'), + 'testentity', + 'id', + ); + + // Create instance of our filter, passing given operators (with lookups for role/category). + [$select, $params] = courserole::create($filter)->get_sql_filter([ + $filter->get_unique_identifier() . '_role' => $DB->get_field('role', 'id', ['shortname' => $rolename]), + $filter->get_unique_identifier() . '_category' => $DB->get_field('course_categories', 'id', ['name' => $categoryname]), + $filter->get_unique_identifier() . '_course' => $course, + ]); + + $users = $DB->get_fieldset_select('user', 'username', $select, $params); + $this->assertEqualsCanonicalizing($expectedusers, $users); + } +}