2 // This file is part of Moodle - https://moodle.org/
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
18 * Provides {@link core_user_table_participants_search_test} class.
22 * @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26 declare(strict_types
=1);
28 namespace core_user\table
;
30 use advanced_testcase
;
32 use context_coursecat
;
33 use core_table\local\filter\filter
;
34 use core_table\local\filter\integer_filter
;
35 use core_table\local\filter\string_filter
;
36 use core_user\table\participants_filterset
;
37 use core_user\table\participants_search
;
42 * Tests for the implementation of {@link core_user_table_participants_search} class.
44 * @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
45 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
47 class participants_search_test
extends advanced_testcase
{
50 * Helper to convert a moodle_recordset to an array of records.
52 * @param moodle_recordset $recordset
55 protected function convert_recordset_to_array(moodle_recordset
$recordset): array {
57 foreach ($recordset as $record) {
58 $records[$record->id
] = $record;
66 * Create and enrol a set of users into the specified course.
68 * @param stdClass $course
70 * @param null|string $role
73 protected function create_and_enrol_users(stdClass
$course, int $count, ?
string $role = null): array {
74 $this->resetAfterTest(true);
77 for ($i = 0; $i < $count; $i++
) {
78 $user = $this->getDataGenerator()->create_user();
79 $this->getDataGenerator()->enrol_user($user->id
, $course->id
, $role);
87 * Create a new course with several types of user.
89 * @param int $editingteachers The number of editing teachers to create in the course.
90 * @param int $teachers The number of non-editing teachers to create in the course.
91 * @param int $students The number of students to create in the course.
92 * @param int $norole The number of users with no role to create in the course.
95 protected function create_course_with_users(int $editingteachers, int $teachers, int $students, int $norole): stdClass
{
97 'course' => $this->getDataGenerator()->create_course(),
98 'editingteachers' => [],
104 $data->context
= context_course
::instance($data->course
->id
);
106 $data->editingteachers
= $this->create_and_enrol_users($data->course
, $editingteachers, 'editingteacher');
107 $data->teachers
= $this->create_and_enrol_users($data->course
, $teachers, 'teacher');
108 $data->students
= $this->create_and_enrol_users($data->course
, $students, 'student');
109 $data->norole
= $this->create_and_enrol_users($data->course
, $norole);
114 * Ensure that the roles filter works as expected with the provided test cases.
116 * @param array $usersdata The list of users and their roles to create
117 * @param array $testroles The list of roles to filter by
118 * @param int $jointype The join type to use when combining filter values
119 * @param int $count The expected count
120 * @param array $expectedusers
121 * @dataProvider role_provider
123 public function test_roles_filter(array $usersdata, array $testroles, int $jointype, int $count, array $expectedusers): void
{
126 $roles = $DB->get_records_menu('role', [], '', 'shortname, id');
128 // Remove the default role.
129 set_config('roleid', 0, 'enrol_manual');
131 $course = $this->getDataGenerator()->create_course();
132 $coursecontext = context_course
::instance($course->id
);
134 $category = $DB->get_record('course_categories', ['id' => $course->category
]);
135 $categorycontext = context_coursecat
::instance($category->id
);
139 foreach ($usersdata as $username => $userdata) {
140 $user = $this->getDataGenerator()->create_user(['username' => $username]);
142 if (array_key_exists('courseroles', $userdata)) {
143 $this->getDataGenerator()->enrol_user($user->id
, $course->id
, null);
144 foreach ($userdata['courseroles'] as $rolename) {
145 $this->getDataGenerator()->role_assign($roles[$rolename], $user->id
, $coursecontext->id
);
149 if (array_key_exists('categoryroles', $userdata)) {
150 foreach ($userdata['categoryroles'] as $rolename) {
151 $this->getDataGenerator()->role_assign($roles[$rolename], $user->id
, $categorycontext->id
);
154 $users[$username] = $user;
157 // Create a secondary course with users. We should not see these users.
158 $this->create_course_with_users(1, 1, 1, 1);
160 // Create the basic filter.
161 $filterset = new participants_filterset();
162 $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id
]));
164 // Create the role filter.
165 $rolefilter = new integer_filter('roles');
166 $filterset->add_filter($rolefilter);
168 // Configure the filter.
169 foreach ($testroles as $rolename) {
170 $rolefilter->add_filter_value((int) $roles[$rolename]);
172 $rolefilter->set_join_type($jointype);
175 $search = new participants_search($course, $coursecontext, $filterset);
176 $rs = $search->get_participants();
177 $this->assertInstanceOf(moodle_recordset
::class, $rs);
178 $records = $this->convert_recordset_to_array($rs);
180 $this->assertCount($count, $records);
181 $this->assertEquals($count, $search->get_total_participants_count());
183 foreach ($expectedusers as $expecteduser) {
184 $this->assertArrayHasKey($users[$expecteduser]->id
, $records);
189 * Data provider for role tests.
193 public function role_provider(): array {
195 // Users who only have one role each.
196 'Users in each role' => (object) [
228 // User is enrolled in the course without role.
234 // User is a category manager and also enrolled without role in the course.
243 // User is a category manager and not enrolled in the course.
244 // This user should not show up in any filter.
252 // Tests for jointype: ANY.
253 'ANY: No role filter' => (object) [
255 'jointype' => filter
::JOINTYPE_ANY
,
268 'ANY: Filter on student' => (object) [
269 'roles' => ['student'],
270 'jointype' => filter
::JOINTYPE_ANY
,
277 'ANY: Filter on student, teacher' => (object) [
278 'roles' => ['student', 'teacher'],
279 'jointype' => filter
::JOINTYPE_ANY
,
288 'ANY: Filter on student, manager (category level role)' => (object) [
289 'roles' => ['student', 'manager'],
290 'jointype' => filter
::JOINTYPE_ANY
,
298 'ANY: Filter on student, coursecreator (not assigned)' => (object) [
299 'roles' => ['student', 'coursecreator'],
300 'jointype' => filter
::JOINTYPE_ANY
,
308 // Tests for jointype: ALL.
309 'ALL: No role filter' => (object) [
311 'jointype' => filter
::JOINTYPE_ALL
,
324 'ALL: Filter on student' => (object) [
325 'roles' => ['student'],
326 'jointype' => filter
::JOINTYPE_ALL
,
333 'ALL: Filter on student, teacher' => (object) [
334 'roles' => ['student', 'teacher'],
335 'jointype' => filter
::JOINTYPE_ALL
,
337 'expectedusers' => [],
339 'ALL: Filter on student, manager (category level role))' => (object) [
340 'roles' => ['student', 'manager'],
341 'jointype' => filter
::JOINTYPE_ALL
,
343 'expectedusers' => [],
345 'ALL: Filter on student, coursecreator (not assigned))' => (object) [
346 'roles' => ['student', 'coursecreator'],
347 'jointype' => filter
::JOINTYPE_ALL
,
349 'expectedusers' => [],
352 // Tests for jointype: NONE.
353 'NONE: No role filter' => (object) [
355 'jointype' => filter
::JOINTYPE_NONE
,
368 'NONE: Filter on student' => (object) [
369 'roles' => ['student'],
370 'jointype' => filter
::JOINTYPE_NONE
,
381 'NONE: Filter on student, teacher' => (object) [
382 'roles' => ['student', 'teacher'],
383 'jointype' => filter
::JOINTYPE_NONE
,
392 'NONE: Filter on student, manager (category level role))' => (object) [
393 'roles' => ['student', 'manager'],
394 'jointype' => filter
::JOINTYPE_NONE
,
404 'NONE: Filter on student, coursecreator (not assigned))' => (object) [
405 'roles' => ['student', 'coursecreator'],
406 'jointype' => filter
::JOINTYPE_NONE
,
419 'Users with multiple roles' => (object) [
454 // User is enrolled in the course without role.
460 // User is a category manager and also enrolled without role in the course.
469 // User is a category manager and not enrolled in the course.
470 // This user should not show up in any filter.
478 // Tests for jointype: ANY.
479 'ANY: No role filter' => (object) [
481 'jointype' => filter
::JOINTYPE_ANY
,
494 'ANY: Filter on student' => (object) [
495 'roles' => ['student'],
496 'jointype' => filter
::JOINTYPE_ANY
,
503 'ANY: Filter on teacher' => (object) [
504 'roles' => ['teacher'],
505 'jointype' => filter
::JOINTYPE_ANY
,
513 'ANY: Filter on editingteacher' => (object) [
514 'roles' => ['editingteacher'],
515 'jointype' => filter
::JOINTYPE_ANY
,
523 'ANY: Filter on student, teacher' => (object) [
524 'roles' => ['student', 'teacher'],
525 'jointype' => filter
::JOINTYPE_ANY
,
534 'ANY: Filter on teacher, editingteacher' => (object) [
535 'roles' => ['teacher', 'editingteacher'],
536 'jointype' => filter
::JOINTYPE_ANY
,
546 'ANY: Filter on student, manager (category level role)' => (object) [
547 'roles' => ['student', 'manager'],
548 'jointype' => filter
::JOINTYPE_ANY
,
556 'ANY: Filter on student, coursecreator (not assigned)' => (object) [
557 'roles' => ['student', 'coursecreator'],
558 'jointype' => filter
::JOINTYPE_ANY
,
566 // Tests for jointype: ALL.
567 'ALL: No role filter' => (object) [
569 'jointype' => filter
::JOINTYPE_ALL
,
582 'ALL: Filter on student' => (object) [
583 'roles' => ['student'],
584 'jointype' => filter
::JOINTYPE_ALL
,
591 'ALL: Filter on teacher' => (object) [
592 'roles' => ['teacher'],
593 'jointype' => filter
::JOINTYPE_ALL
,
601 'ALL: Filter on editingteacher' => (object) [
602 'roles' => ['editingteacher'],
603 'jointype' => filter
::JOINTYPE_ALL
,
611 'ALL: Filter on student, teacher' => (object) [
612 'roles' => ['student', 'teacher'],
613 'jointype' => filter
::JOINTYPE_ALL
,
619 'ALL: Filter on teacher, editingteacher' => (object) [
620 'roles' => ['teacher', 'editingteacher'],
621 'jointype' => filter
::JOINTYPE_ALL
,
627 'ALL: Filter on student, manager (category level role)' => (object) [
628 'roles' => ['student', 'manager'],
629 'jointype' => filter
::JOINTYPE_ALL
,
631 'expectedusers' => [],
633 'ALL: Filter on student, coursecreator (not assigned)' => (object) [
634 'roles' => ['student', 'coursecreator'],
635 'jointype' => filter
::JOINTYPE_ALL
,
637 'expectedusers' => [],
640 // Tests for jointype: NONE.
641 'NONE: No role filter' => (object) [
643 'jointype' => filter
::JOINTYPE_NONE
,
656 'NONE: Filter on student' => (object) [
657 'roles' => ['student'],
658 'jointype' => filter
::JOINTYPE_NONE
,
669 'NONE: Filter on teacher' => (object) [
670 'roles' => ['teacher'],
671 'jointype' => filter
::JOINTYPE_NONE
,
681 'NONE: Filter on editingteacher' => (object) [
682 'roles' => ['editingteacher'],
683 'jointype' => filter
::JOINTYPE_NONE
,
693 'NONE: Filter on student, teacher' => (object) [
694 'roles' => ['student', 'teacher'],
695 'jointype' => filter
::JOINTYPE_NONE
,
705 'NONE: Filter on student, teacher' => (object) [
706 'roles' => ['teacher', 'editingteacher'],
707 'jointype' => filter
::JOINTYPE_NONE
,
715 'NONE: Filter on student, manager (category level role)' => (object) [
716 'roles' => ['student', 'manager'],
717 'jointype' => filter
::JOINTYPE_NONE
,
727 'NONE: Filter on student, coursecreator (not assigned)' => (object) [
728 'roles' => ['student', 'coursecreator'],
729 'jointype' => filter
::JOINTYPE_NONE
,
745 foreach ($tests as $testname => $testdata) {
746 foreach ($testdata->expect
as $expectname => $expectdata) {
747 $finaltests["{$testname} => {$expectname}"] = [
748 'users' => $testdata->users
,
749 'roles' => $expectdata->roles
,
750 'jointype' => $expectdata->jointype
,
751 'count' => $expectdata->count
,
752 'expectedusers' => $expectdata->expectedusers
,
761 * Ensure that the keywords filter works as expected with the provided test cases.
763 * @param array $usersdata The list of users to create
764 * @param array $keywords The list of keywords to filter by
765 * @param int $jointype The join type to use when combining filter values
766 * @param int $count The expected count
767 * @param array $expectedusers
768 * @dataProvider keywords_provider
770 public function test_keywords_filter(array $usersdata, array $keywords, int $jointype, int $count, array $expectedusers): void
{
771 $course = $this->getDataGenerator()->create_course();
772 $coursecontext = context_course
::instance($course->id
);
775 foreach ($usersdata as $username => $userdata) {
776 // Prevent randomly generated field values that may cause false fails.
777 $userdata['firstnamephonetic'] = $userdata['firstnamephonetic'] ??
$userdata['firstname'];
778 $userdata['lastnamephonetic'] = $userdata['lastnamephonetic'] ??
$userdata['lastname'];
779 $userdata['middlename'] = $userdata['middlename'] ??
'';
780 $userdata['alternatename'] = $userdata['alternatename'] ??
$username;
782 $user = $this->getDataGenerator()->create_user($userdata);
783 $this->getDataGenerator()->enrol_user($user->id
, $course->id
, 'student');
784 $users[$username] = $user;
787 // Create a secondary course with users. We should not see these users.
788 $this->create_course_with_users(10, 10, 10, 10);
790 // Create the basic filter.
791 $filterset = new participants_filterset();
792 $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id
]));
794 // Create the keyword filter.
795 $keywordfilter = new string_filter('keywords');
796 $filterset->add_filter($keywordfilter);
798 // Configure the filter.
799 foreach ($keywords as $keyword) {
800 $keywordfilter->add_filter_value($keyword);
802 $keywordfilter->set_join_type($jointype);
805 $search = new participants_search($course, $coursecontext, $filterset);
806 $rs = $search->get_participants();
807 $this->assertInstanceOf(moodle_recordset
::class, $rs);
808 $records = $this->convert_recordset_to_array($rs);
810 $this->assertCount($count, $records);
811 $this->assertEquals($count, $search->get_total_participants_count());
813 foreach ($expectedusers as $expecteduser) {
814 $this->assertArrayHasKey($users[$expecteduser]->id
, $records);
819 * Data provider for keywords tests.
823 public function keywords_provider(): array {
825 // Users where the keyword matches basic user fields such as names and email.
826 'Users with basic names' => (object) [
829 'firstname' => 'Adam',
832 'barbara.bennett' => [
833 'firstname' => 'Barbara',
834 'lastname' => 'Bennett',
835 'alternatename' => 'Babs',
836 'firstnamephonetic' => 'Barbra',
837 'lastnamephonetic' => 'Benit',
839 'colin.carnforth' => [
840 'firstname' => 'Colin',
841 'lastname' => 'Carnforth',
842 'middlename' => 'Jeffery',
845 'firstname' => 'Anthony',
846 'lastname' => 'Rogers',
847 'lastnamephonetic' => 'Rowjours',
850 'firstname' => 'Sarah',
851 'lastname' => 'Rester',
852 'email' => 'zazu@example.com',
853 'firstnamephonetic' => 'Sera',
857 // Tests for jointype: ANY.
858 'ANY: No filter' => (object) [
860 'jointype' => filter
::JOINTYPE_ANY
,
870 'ANY: Filter on first name only' => (object) [
871 'keywords' => ['adam'],
872 'jointype' => filter
::JOINTYPE_ANY
,
878 'ANY: Filter on last name only' => (object) [
879 'keywords' => ['BeNNeTt'],
880 'jointype' => filter
::JOINTYPE_ANY
,
886 'ANY: Filter on first/Last name' => (object) [
887 'keywords' => ['ant'],
888 'jointype' => filter
::JOINTYPE_ANY
,
895 'ANY: Filter on middlename only' => (object) [
896 'keywords' => ['Jeff'],
897 'jointype' => filter
::JOINTYPE_ANY
,
903 'ANY: Filter on username (no match)' => (object) [
904 'keywords' => ['sara.rester'],
905 'jointype' => filter
::JOINTYPE_ANY
,
907 'expectedusers' => [],
909 'ANY: Filter on email only' => (object) [
910 'keywords' => ['zazu'],
911 'jointype' => filter
::JOINTYPE_ANY
,
917 'ANY: Filter on first name phonetic only' => (object) [
918 'keywords' => ['Sera'],
919 'jointype' => filter
::JOINTYPE_ANY
,
925 'ANY: Filter on last name phonetic only' => (object) [
926 'keywords' => ['jour'],
927 'jointype' => filter
::JOINTYPE_ANY
,
933 'ANY: Filter on alternate name only' => (object) [
934 'keywords' => ['Babs'],
935 'jointype' => filter
::JOINTYPE_ANY
,
941 'ANY: Filter on multiple keywords (first/middle/last name)' => (object) [
942 'keywords' => ['ant', 'Jeff', 'rog'],
943 'jointype' => filter
::JOINTYPE_ANY
,
951 'ANY: Filter on multiple keywords (phonetic/alternate names)' => (object) [
952 'keywords' => ['era', 'Bab', 'ours'],
953 'jointype' => filter
::JOINTYPE_ANY
,
962 // Tests for jointype: ALL.
963 'ALL: No filter' => (object) [
965 'jointype' => filter
::JOINTYPE_ALL
,
975 'ALL: Filter on first name only' => (object) [
976 'keywords' => ['adam'],
977 'jointype' => filter
::JOINTYPE_ALL
,
983 'ALL: Filter on last name only' => (object) [
984 'keywords' => ['BeNNeTt'],
985 'jointype' => filter
::JOINTYPE_ALL
,
991 'ALL: Filter on first/Last name' => (object) [
992 'keywords' => ['ant'],
993 'jointype' => filter
::JOINTYPE_ALL
,
1000 'ALL: Filter on middlename only' => (object) [
1001 'keywords' => ['Jeff'],
1002 'jointype' => filter
::JOINTYPE_ALL
,
1004 'expectedusers' => [
1008 'ALL: Filter on username (no match)' => (object) [
1009 'keywords' => ['sara.rester'],
1010 'jointype' => filter
::JOINTYPE_ALL
,
1012 'expectedusers' => [],
1014 'ALL: Filter on email only' => (object) [
1015 'keywords' => ['zazu'],
1016 'jointype' => filter
::JOINTYPE_ALL
,
1018 'expectedusers' => [
1022 'ALL: Filter on first name phonetic only' => (object) [
1023 'keywords' => ['Sera'],
1024 'jointype' => filter
::JOINTYPE_ALL
,
1026 'expectedusers' => [
1030 'ALL: Filter on last name phonetic only' => (object) [
1031 'keywords' => ['jour'],
1032 'jointype' => filter
::JOINTYPE_ALL
,
1034 'expectedusers' => [
1038 'ALL: Filter on alternate name only' => (object) [
1039 'keywords' => ['Babs'],
1040 'jointype' => filter
::JOINTYPE_ALL
,
1042 'expectedusers' => [
1046 'ALL: Filter on multiple keywords (first/last name)' => (object) [
1047 'keywords' => ['ant', 'rog'],
1048 'jointype' => filter
::JOINTYPE_ALL
,
1050 'expectedusers' => [
1054 'ALL: Filter on multiple keywords (first/middle/last name)' => (object) [
1055 'keywords' => ['ant', 'Jeff', 'rog'],
1056 'jointype' => filter
::JOINTYPE_ALL
,
1058 'expectedusers' => [],
1060 'ALL: Filter on multiple keywords (phonetic/alternate names)' => (object) [
1061 'keywords' => ['Bab', 'bra', 'nit'],
1062 'jointype' => filter
::JOINTYPE_ALL
,
1064 'expectedusers' => [
1069 // Tests for jointype: NONE.
1070 'NONE: No filter' => (object) [
1072 'jointype' => filter
::JOINTYPE_NONE
,
1074 'expectedusers' => [
1082 'NONE: Filter on first name only' => (object) [
1083 'keywords' => ['ara'],
1084 'jointype' => filter
::JOINTYPE_NONE
,
1086 'expectedusers' => [
1092 'NONE: Filter on last name only' => (object) [
1093 'keywords' => ['BeNNeTt'],
1094 'jointype' => filter
::JOINTYPE_NONE
,
1096 'expectedusers' => [
1103 'NONE: Filter on first/Last name' => (object) [
1104 'keywords' => ['ar'],
1105 'jointype' => filter
::JOINTYPE_NONE
,
1107 'expectedusers' => [
1112 'NONE: Filter on middlename only' => (object) [
1113 'keywords' => ['Jeff'],
1114 'jointype' => filter
::JOINTYPE_NONE
,
1116 'expectedusers' => [
1123 'NONE: Filter on username (no match)' => (object) [
1124 'keywords' => ['sara.rester'],
1125 'jointype' => filter
::JOINTYPE_NONE
,
1127 'expectedusers' => [
1135 'NONE: Filter on email' => (object) [
1136 'keywords' => ['zazu'],
1137 'jointype' => filter
::JOINTYPE_NONE
,
1139 'expectedusers' => [
1146 'NONE: Filter on first name phonetic only' => (object) [
1147 'keywords' => ['Sera'],
1148 'jointype' => filter
::JOINTYPE_NONE
,
1150 'expectedusers' => [
1157 'NONE: Filter on last name phonetic only' => (object) [
1158 'keywords' => ['jour'],
1159 'jointype' => filter
::JOINTYPE_NONE
,
1161 'expectedusers' => [
1168 'NONE: Filter on alternate name only' => (object) [
1169 'keywords' => ['Babs'],
1170 'jointype' => filter
::JOINTYPE_NONE
,
1172 'expectedusers' => [
1179 'NONE: Filter on multiple keywords (first/last name)' => (object) [
1180 'keywords' => ['ara', 'rog'],
1181 'jointype' => filter
::JOINTYPE_NONE
,
1183 'expectedusers' => [
1188 'NONE: Filter on multiple keywords (first/middle/last name)' => (object) [
1189 'keywords' => ['ant', 'Jeff', 'rog'],
1190 'jointype' => filter
::JOINTYPE_NONE
,
1192 'expectedusers' => [
1197 'NONE: Filter on multiple keywords (phonetic/alternate names)' => (object) [
1198 'keywords' => ['Bab', 'bra', 'nit'],
1199 'jointype' => filter
::JOINTYPE_NONE
,
1201 'expectedusers' => [
1213 foreach ($tests as $testname => $testdata) {
1214 foreach ($testdata->expect
as $expectname => $expectdata) {
1215 $finaltests["{$testname} => {$expectname}"] = [
1216 'users' => $testdata->users
,
1217 'keywords' => $expectdata->keywords
,
1218 'jointype' => $expectdata->jointype
,
1219 'count' => $expectdata->count
,
1220 'expectedusers' => $expectdata->expectedusers
,
1229 * Ensure that the enrolment status filter works as expected with the provided test cases.
1231 * @param array $usersdata The list of users to create
1232 * @param array $statuses The list of statuses to filter by
1233 * @param int $jointype The join type to use when combining filter values
1234 * @param int $count The expected count
1235 * @param array $expectedusers
1236 * @dataProvider status_provider
1238 public function test_status_filter(array $usersdata, array $statuses, int $jointype, int $count, array $expectedusers): void
{
1239 $course = $this->getDataGenerator()->create_course();
1240 $coursecontext = context_course
::instance($course->id
);
1243 // Ensure sufficient capabilities to view all statuses.
1244 $this->setAdminUser();
1246 // Ensure all enrolment methods enabled.
1247 $enrolinstances = enrol_get_instances($course->id
, false);
1248 foreach ($enrolinstances as $instance) {
1249 $plugin = enrol_get_plugin($instance->enrol
);
1250 $plugin->update_status($instance, ENROL_INSTANCE_ENABLED
);
1253 foreach ($usersdata as $username => $userdata) {
1254 $user = $this->getDataGenerator()->create_user(['username' => $username]);
1256 if (array_key_exists('statuses', $userdata)) {
1257 foreach ($userdata['statuses'] as $enrolmethod => $status) {
1258 $this->getDataGenerator()->enrol_user($user->id
, $course->id
, 'student', $enrolmethod, 0, 0, $status);
1262 $users[$username] = $user;
1265 // Create a secondary course with users. We should not see these users.
1266 $this->create_course_with_users(1, 1, 1, 1);
1268 // Create the basic filter.
1269 $filterset = new participants_filterset();
1270 $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id
]));
1272 // Create the status filter.
1273 $statusfilter = new integer_filter('status');
1274 $filterset->add_filter($statusfilter);
1276 // Configure the filter.
1277 foreach ($statuses as $status) {
1278 $statusfilter->add_filter_value($status);
1280 $statusfilter->set_join_type($jointype);
1283 $search = new participants_search($course, $coursecontext, $filterset);
1284 $rs = $search->get_participants();
1285 $this->assertInstanceOf(moodle_recordset
::class, $rs);
1286 $records = $this->convert_recordset_to_array($rs);
1288 $this->assertCount($count, $records);
1289 $this->assertEquals($count, $search->get_total_participants_count());
1291 foreach ($expectedusers as $expecteduser) {
1292 $this->assertArrayHasKey($users[$expecteduser]->id
, $records);
1297 * Data provider for status filter tests.
1301 public function status_provider(): array {
1303 // Users with different statuses and enrolment methods (so multiple statuses are possible for the same user).
1304 'Users with different enrolment statuses' => (object) [
1308 'manual' => ENROL_USER_ACTIVE
,
1313 'self' => ENROL_USER_ACTIVE
,
1318 'manual' => ENROL_USER_SUSPENDED
,
1323 'self' => ENROL_USER_SUSPENDED
,
1328 'manual' => ENROL_USER_ACTIVE
,
1329 'self' => ENROL_USER_SUSPENDED
,
1334 // Tests for jointype: ANY.
1335 'ANY: No filter' => (object) [
1337 'jointype' => filter
::JOINTYPE_ANY
,
1339 'expectedusers' => [
1347 'ANY: Filter on active only' => (object) [
1348 'statuses' => [ENROL_USER_ACTIVE
],
1349 'jointype' => filter
::JOINTYPE_ANY
,
1351 'expectedusers' => [
1357 'ANY: Filter on suspended only' => (object) [
1358 'statuses' => [ENROL_USER_SUSPENDED
],
1359 'jointype' => filter
::JOINTYPE_ANY
,
1361 'expectedusers' => [
1367 'ANY: Filter on multiple statuses' => (object) [
1368 'statuses' => [ENROL_USER_ACTIVE
, ENROL_USER_SUSPENDED
],
1369 'jointype' => filter
::JOINTYPE_ANY
,
1371 'expectedusers' => [
1380 // Tests for jointype: ALL.
1381 'ALL: No filter' => (object) [
1383 'jointype' => filter
::JOINTYPE_ALL
,
1385 'expectedusers' => [
1393 'ALL: Filter on active only' => (object) [
1394 'statuses' => [ENROL_USER_ACTIVE
],
1395 'jointype' => filter
::JOINTYPE_ALL
,
1397 'expectedusers' => [
1403 'ALL: Filter on suspended only' => (object) [
1404 'statuses' => [ENROL_USER_SUSPENDED
],
1405 'jointype' => filter
::JOINTYPE_ALL
,
1407 'expectedusers' => [
1413 'ALL: Filter on multiple statuses' => (object) [
1414 'statuses' => [ENROL_USER_ACTIVE
, ENROL_USER_SUSPENDED
],
1415 'jointype' => filter
::JOINTYPE_ALL
,
1417 'expectedusers' => [
1422 // Tests for jointype: NONE.
1423 'NONE: No filter' => (object) [
1425 'jointype' => filter
::JOINTYPE_NONE
,
1427 'expectedusers' => [
1435 'NONE: Filter on active only' => (object) [
1436 'statuses' => [ENROL_USER_ACTIVE
],
1437 'jointype' => filter
::JOINTYPE_NONE
,
1439 'expectedusers' => [
1445 'NONE: Filter on suspended only' => (object) [
1446 'statuses' => [ENROL_USER_SUSPENDED
],
1447 'jointype' => filter
::JOINTYPE_NONE
,
1449 'expectedusers' => [
1455 'NONE: Filter on multiple statuses' => (object) [
1456 'statuses' => [ENROL_USER_ACTIVE
, ENROL_USER_SUSPENDED
],
1457 'jointype' => filter
::JOINTYPE_NONE
,
1459 'expectedusers' => [],
1466 foreach ($tests as $testname => $testdata) {
1467 foreach ($testdata->expect
as $expectname => $expectdata) {
1468 $finaltests["{$testname} => {$expectname}"] = [
1469 'users' => $testdata->users
,
1470 'statuses' => $expectdata->statuses
,
1471 'jointype' => $expectdata->jointype
,
1472 'count' => $expectdata->count
,
1473 'expectedusers' => $expectdata->expectedusers
,
1482 * Ensure that the enrolment methods filter works as expected with the provided test cases.
1484 * @param array $usersdata The list of users to create
1485 * @param array $enrolmethods The list of enrolment methods to filter by
1486 * @param int $jointype The join type to use when combining filter values
1487 * @param int $count The expected count
1488 * @param array $expectedusers
1489 * @dataProvider enrolments_provider
1491 public function test_enrolments_filter(array $usersdata, array $enrolmethods, int $jointype, int $count,
1492 array $expectedusers): void
{
1494 $course = $this->getDataGenerator()->create_course();
1495 $coursecontext = context_course
::instance($course->id
);
1498 // Ensure all enrolment methods enabled and mapped for setting the filter later.
1499 $enrolinstances = enrol_get_instances($course->id
, false);
1500 $enrolinstancesmap = [];
1501 foreach ($enrolinstances as $instance) {
1502 $plugin = enrol_get_plugin($instance->enrol
);
1503 $plugin->update_status($instance, ENROL_INSTANCE_ENABLED
);
1505 $enrolinstancesmap[$instance->enrol
] = (int) $instance->id
;
1508 foreach ($usersdata as $username => $userdata) {
1509 $user = $this->getDataGenerator()->create_user(['username' => $username]);
1511 if (array_key_exists('enrolmethods', $userdata)) {
1512 foreach ($userdata['enrolmethods'] as $enrolmethod) {
1513 $this->getDataGenerator()->enrol_user($user->id
, $course->id
, 'student', $enrolmethod);
1517 $users[$username] = $user;
1520 // Create a secondary course with users. We should not see these users.
1521 $this->create_course_with_users(1, 1, 1, 1);
1523 // Create the basic filter.
1524 $filterset = new participants_filterset();
1525 $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id
]));
1527 // Create the enrolment methods filter.
1528 $enrolmethodfilter = new integer_filter('enrolments');
1529 $filterset->add_filter($enrolmethodfilter);
1531 // Configure the filter.
1532 foreach ($enrolmethods as $enrolmethod) {
1533 $enrolmethodfilter->add_filter_value($enrolinstancesmap[$enrolmethod]);
1535 $enrolmethodfilter->set_join_type($jointype);
1538 $search = new participants_search($course, $coursecontext, $filterset);
1539 $rs = $search->get_participants();
1540 $this->assertInstanceOf(moodle_recordset
::class, $rs);
1541 $records = $this->convert_recordset_to_array($rs);
1543 $this->assertCount($count, $records);
1544 $this->assertEquals($count, $search->get_total_participants_count());
1546 foreach ($expectedusers as $expecteduser) {
1547 $this->assertArrayHasKey($users[$expecteduser]->id
, $records);
1552 * Data provider for enrolments filter tests.
1556 public function enrolments_provider(): array {
1558 // Users with different enrolment methods.
1559 'Users with different enrolment methods' => (object) [
1579 // Tests for jointype: ANY.
1580 'ANY: No filter' => (object) [
1581 'enrolmethods' => [],
1582 'jointype' => filter
::JOINTYPE_ANY
,
1584 'expectedusers' => [
1590 'ANY: Filter by manual enrolments only' => (object) [
1591 'enrolmethods' => ['manual'],
1592 'jointype' => filter
::JOINTYPE_ANY
,
1594 'expectedusers' => [
1599 'ANY: Filter by self enrolments only' => (object) [
1600 'enrolmethods' => ['self'],
1601 'jointype' => filter
::JOINTYPE_ANY
,
1603 'expectedusers' => [
1608 'ANY: Filter by multiple enrolment methods' => (object) [
1609 'enrolmethods' => ['manual', 'self'],
1610 'jointype' => filter
::JOINTYPE_ANY
,
1612 'expectedusers' => [
1619 // Tests for jointype: ALL.
1620 'ALL: No filter' => (object) [
1621 'enrolmethods' => [],
1622 'jointype' => filter
::JOINTYPE_ALL
,
1624 'expectedusers' => [
1630 'ALL: Filter by manual enrolments only' => (object) [
1631 'enrolmethods' => ['manual'],
1632 'jointype' => filter
::JOINTYPE_ALL
,
1634 'expectedusers' => [
1639 'ALL: Filter by multiple enrolment methods' => (object) [
1640 'enrolmethods' => ['manual', 'self'],
1641 'jointype' => filter
::JOINTYPE_ALL
,
1643 'expectedusers' => [
1648 // Tests for jointype: NONE.
1649 'NONE: No filter' => (object) [
1650 'enrolmethods' => [],
1651 'jointype' => filter
::JOINTYPE_NONE
,
1653 'expectedusers' => [
1659 'NONE: Filter by manual enrolments only' => (object) [
1660 'enrolmethods' => ['manual'],
1661 'jointype' => filter
::JOINTYPE_NONE
,
1663 'expectedusers' => [
1667 'NONE: Filter by multiple enrolment methods' => (object) [
1668 'enrolmethods' => ['manual', 'self'],
1669 'jointype' => filter
::JOINTYPE_NONE
,
1671 'expectedusers' => [],
1678 foreach ($tests as $testname => $testdata) {
1679 foreach ($testdata->expect
as $expectname => $expectdata) {
1680 $finaltests["{$testname} => {$expectname}"] = [
1681 'users' => $testdata->users
,
1682 'enrolmethods' => $expectdata->enrolmethods
,
1683 'jointype' => $expectdata->jointype
,
1684 'count' => $expectdata->count
,
1685 'expectedusers' => $expectdata->expectedusers
,
1694 * Ensure that the groups filter works as expected with the provided test cases.
1696 * @param array $usersdata The list of users to create
1697 * @param array $groupsavailable The names of groups that should be created in the course
1698 * @param array $filtergroups The names of groups to filter by
1699 * @param int $jointype The join type to use when combining filter values
1700 * @param int $count The expected count
1701 * @param array $expectedusers
1702 * @dataProvider groups_provider
1704 public function test_groups_filter(array $usersdata, array $groupsavailable, array $filtergroups, int $jointype, int $count,
1705 array $expectedusers): void
{
1707 $course = $this->getDataGenerator()->create_course();
1708 $coursecontext = context_course
::instance($course->id
);
1711 // Prepare data for filtering by users in no groups.
1712 $nogroupsdata = (object) [
1713 'id' => USERSWITHOUTGROUP
,
1716 // Map group names to group data.
1717 $groupsdata = ['nogroups' => $nogroupsdata];
1718 foreach ($groupsavailable as $groupname) {
1720 'courseid' => $course->id
,
1721 'name' => $groupname,
1724 $groupsdata[$groupname] = $this->getDataGenerator()->create_group($groupinfo);
1727 foreach ($usersdata as $username => $userdata) {
1728 $user = $this->getDataGenerator()->create_user(['username' => $username]);
1729 $this->getDataGenerator()->enrol_user($user->id
, $course->id
, 'student');
1731 if (array_key_exists('groups', $userdata)) {
1732 foreach ($userdata['groups'] as $groupname) {
1734 'userid' => $user->id
,
1735 'groupid' => (int) $groupsdata[$groupname]->id
,
1737 $this->getDataGenerator()->create_group_member($userinfo);
1741 $users[$username] = $user;
1744 // Create a secondary course with users. We should not see these users.
1745 $this->create_course_with_users(1, 1, 1, 1);
1747 // Create the basic filter.
1748 $filterset = new participants_filterset();
1749 $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id
]));
1751 // Create the groups filter.
1752 $groupsfilter = new integer_filter('groups');
1753 $filterset->add_filter($groupsfilter);
1755 // Configure the filter.
1756 foreach ($filtergroups as $filtergroupname) {
1757 $groupsfilter->add_filter_value((int) $groupsdata[$filtergroupname]->id
);
1759 $groupsfilter->set_join_type($jointype);
1762 $search = new participants_search($course, $coursecontext, $filterset);
1763 $rs = $search->get_participants();
1764 $this->assertInstanceOf(moodle_recordset
::class, $rs);
1765 $records = $this->convert_recordset_to_array($rs);
1767 $this->assertCount($count, $records);
1768 $this->assertEquals($count, $search->get_total_participants_count());
1770 foreach ($expectedusers as $expecteduser) {
1771 $this->assertArrayHasKey($users[$expecteduser]->id
, $records);
1776 * Data provider for groups filter tests.
1780 public function groups_provider(): array {
1782 'Users in different groups' => (object) [
1783 'groupsavailable' => [
1790 'groups' => ['groupa'],
1793 'groups' => ['groupb'],
1796 'groups' => ['groupa', 'groupb'],
1803 // Tests for jointype: ANY.
1804 'ANY: No filter' => (object) [
1806 'jointype' => filter
::JOINTYPE_ANY
,
1808 'expectedusers' => [
1815 'ANY: Filter on a single group' => (object) [
1816 'groups' => ['groupa'],
1817 'jointype' => filter
::JOINTYPE_ANY
,
1819 'expectedusers' => [
1824 'ANY: Filter on a group with no members' => (object) [
1825 'groups' => ['groupc'],
1826 'jointype' => filter
::JOINTYPE_ANY
,
1828 'expectedusers' => [],
1830 'ANY: Filter on multiple groups' => (object) [
1831 'groups' => ['groupa', 'groupb'],
1832 'jointype' => filter
::JOINTYPE_ANY
,
1834 'expectedusers' => [
1840 'ANY: Filter on members of no groups only' => (object) [
1841 'groups' => ['nogroups'],
1842 'jointype' => filter
::JOINTYPE_ANY
,
1844 'expectedusers' => [
1848 'ANY: Filter on a single group or no groups' => (object) [
1849 'groups' => ['groupa', 'nogroups'],
1850 'jointype' => filter
::JOINTYPE_ANY
,
1852 'expectedusers' => [
1858 'ANY: Filter on multiple groups or no groups' => (object) [
1859 'groups' => ['groupa', 'groupb', 'nogroups'],
1860 'jointype' => filter
::JOINTYPE_ANY
,
1862 'expectedusers' => [
1870 // Tests for jointype: ALL.
1871 'ALL: No filter' => (object) [
1873 'jointype' => filter
::JOINTYPE_ALL
,
1875 'expectedusers' => [
1882 'ALL: Filter on a single group' => (object) [
1883 'groups' => ['groupa'],
1884 'jointype' => filter
::JOINTYPE_ALL
,
1886 'expectedusers' => [
1891 'ALL: Filter on a group with no members' => (object) [
1892 'groups' => ['groupc'],
1893 'jointype' => filter
::JOINTYPE_ALL
,
1895 'expectedusers' => [],
1897 'ALL: Filter on members of no groups only' => (object) [
1898 'groups' => ['nogroups'],
1899 'jointype' => filter
::JOINTYPE_ALL
,
1901 'expectedusers' => [
1905 'ALL: Filter on multiple groups' => (object) [
1906 'groups' => ['groupa', 'groupb'],
1907 'jointype' => filter
::JOINTYPE_ALL
,
1909 'expectedusers' => [
1913 'ALL: Filter on a single group and no groups' => (object) [
1914 'groups' => ['groupa', 'nogroups'],
1915 'jointype' => filter
::JOINTYPE_ALL
,
1917 'expectedusers' => [],
1919 'ALL: Filter on multiple groups and no groups' => (object) [
1920 'groups' => ['groupa', 'groupb', 'nogroups'],
1921 'jointype' => filter
::JOINTYPE_ALL
,
1923 'expectedusers' => [],
1926 // Tests for jointype: NONE.
1927 'NONE: No filter' => (object) [
1929 'jointype' => filter
::JOINTYPE_NONE
,
1931 'expectedusers' => [
1938 'NONE: Filter on a single group' => (object) [
1939 'groups' => ['groupa'],
1940 'jointype' => filter
::JOINTYPE_NONE
,
1942 'expectedusers' => [
1947 'NONE: Filter on a group with no members' => (object) [
1948 'groups' => ['groupc'],
1949 'jointype' => filter
::JOINTYPE_NONE
,
1951 'expectedusers' => [
1958 'NONE: Filter on members of no groups only' => (object) [
1959 'groups' => ['nogroups'],
1960 'jointype' => filter
::JOINTYPE_NONE
,
1962 'expectedusers' => [
1968 'NONE: Filter on multiple groups' => (object) [
1969 'groups' => ['groupa', 'groupb'],
1970 'jointype' => filter
::JOINTYPE_NONE
,
1972 'expectedusers' => [
1976 'NONE: Filter on a single group and no groups' => (object) [
1977 'groups' => ['groupa', 'nogroups'],
1978 'jointype' => filter
::JOINTYPE_NONE
,
1980 'expectedusers' => [
1984 'NONE: Filter on multiple groups and no groups' => (object) [
1985 'groups' => ['groupa', 'groupb', 'nogroups'],
1986 'jointype' => filter
::JOINTYPE_NONE
,
1988 'expectedusers' => [],
1995 foreach ($tests as $testname => $testdata) {
1996 foreach ($testdata->expect
as $expectname => $expectdata) {
1997 $finaltests["{$testname} => {$expectname}"] = [
1998 'users' => $testdata->users
,
1999 'groupsavailable' => $testdata->groupsavailable
,
2000 'filtergroups' => $expectdata->groups
,
2001 'jointype' => $expectdata->jointype
,
2002 'count' => $expectdata->count
,
2003 'expectedusers' => $expectdata->expectedusers
,
2012 * Ensure that the last access filter works as expected with the provided test cases.
2014 * @param array $usersdata The list of users to create
2015 * @param array $accesssince The last access data to filter by
2016 * @param int $jointype The join type to use when combining filter values
2017 * @param int $count The expected count
2018 * @param array $expectedusers
2019 * @dataProvider accesssince_provider
2021 public function test_accesssince_filter(array $usersdata, array $accesssince, int $jointype, int $count,
2022 array $expectedusers): void
{
2024 $course = $this->getDataGenerator()->create_course();
2025 $coursecontext = context_course
::instance($course->id
);
2028 foreach ($usersdata as $username => $userdata) {
2029 $usertimestamp = empty($userdata['lastlogin']) ?
0 : strtotime($userdata['lastlogin']);
2031 $user = $this->getDataGenerator()->create_user(['username' => $username]);
2032 $this->getDataGenerator()->enrol_user($user->id
, $course->id
, 'student');
2034 // Create the record of the user's last access to the course.
2035 if ($usertimestamp > 0) {
2036 $this->getDataGenerator()->create_user_course_lastaccess($user, $course, $usertimestamp);
2039 $users[$username] = $user;
2042 // Create a secondary course with users. We should not see these users.
2043 $this->create_course_with_users(1, 1, 1, 1);
2045 // Create the basic filter.
2046 $filterset = new participants_filterset();
2047 $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id
]));
2049 // Create the last access filter.
2050 $lastaccessfilter = new integer_filter('accesssince');
2051 $filterset->add_filter($lastaccessfilter);
2053 // Configure the filter.
2054 foreach ($accesssince as $accessstring) {
2055 $lastaccessfilter->add_filter_value(strtotime($accessstring));
2057 $lastaccessfilter->set_join_type($jointype);
2060 $search = new participants_search($course, $coursecontext, $filterset);
2061 $rs = $search->get_participants();
2062 $this->assertInstanceOf(moodle_recordset
::class, $rs);
2063 $records = $this->convert_recordset_to_array($rs);
2065 $this->assertCount($count, $records);
2066 $this->assertEquals($count, $search->get_total_participants_count());
2068 foreach ($expectedusers as $expecteduser) {
2069 $this->assertArrayHasKey($users[$expecteduser]->id
, $records);
2074 * Data provider for last access filter tests.
2078 public function accesssince_provider(): array {
2080 // Users with different last access times.
2081 'Users in different groups' => (object) [
2084 'lastlogin' => '-3 days',
2087 'lastlogin' => '-2 weeks',
2090 'lastlogin' => '-5 months',
2093 'lastlogin' => '-11 months',
2101 // Tests for jointype: ANY.
2102 'ANY: No filter' => (object) [
2103 'accesssince' => [],
2104 'jointype' => filter
::JOINTYPE_ANY
,
2106 'expectedusers' => [
2114 'ANY: Filter on last login more than 1 year ago' => (object) [
2115 'accesssince' => ['-1 year'],
2116 'jointype' => filter
::JOINTYPE_ANY
,
2118 'expectedusers' => [
2122 'ANY: Filter on last login more than 6 months ago' => (object) [
2123 'accesssince' => ['-6 months'],
2124 'jointype' => filter
::JOINTYPE_ANY
,
2126 'expectedusers' => [
2131 'ANY: Filter on last login more than 3 weeks ago' => (object) [
2132 'accesssince' => ['-3 weeks'],
2133 'jointype' => filter
::JOINTYPE_ANY
,
2135 'expectedusers' => [
2141 'ANY: Filter on last login more than 5 days ago' => (object) [
2142 'accesssince' => ['-5 days'],
2143 'jointype' => filter
::JOINTYPE_ANY
,
2145 'expectedusers' => [
2152 'ANY: Filter on last login more than 2 days ago' => (object) [
2153 'accesssince' => ['-2 days'],
2154 'jointype' => filter
::JOINTYPE_ANY
,
2156 'expectedusers' => [
2165 // Tests for jointype: ALL.
2166 'ALL: No filter' => (object) [
2167 'accesssince' => [],
2168 'jointype' => filter
::JOINTYPE_ALL
,
2170 'expectedusers' => [
2178 'ALL: Filter on last login more than 1 year ago' => (object) [
2179 'accesssince' => ['-1 year'],
2180 'jointype' => filter
::JOINTYPE_ALL
,
2182 'expectedusers' => [
2186 'ALL: Filter on last login more than 6 months ago' => (object) [
2187 'accesssince' => ['-6 months'],
2188 'jointype' => filter
::JOINTYPE_ALL
,
2190 'expectedusers' => [
2195 'ALL: Filter on last login more than 3 weeks ago' => (object) [
2196 'accesssince' => ['-3 weeks'],
2197 'jointype' => filter
::JOINTYPE_ALL
,
2199 'expectedusers' => [
2205 'ALL: Filter on last login more than 5 days ago' => (object) [
2206 'accesssince' => ['-5 days'],
2207 'jointype' => filter
::JOINTYPE_ALL
,
2209 'expectedusers' => [
2216 'ALL: Filter on last login more than 2 days ago' => (object) [
2217 'accesssince' => ['-2 days'],
2218 'jointype' => filter
::JOINTYPE_ALL
,
2220 'expectedusers' => [
2229 // Tests for jointype: NONE.
2230 'NONE: No filter' => (object) [
2231 'accesssince' => [],
2232 'jointype' => filter
::JOINTYPE_NONE
,
2234 'expectedusers' => [
2242 'NONE: Filter on last login more than 1 year ago' => (object) [
2243 'accesssince' => ['-1 year'],
2244 'jointype' => filter
::JOINTYPE_NONE
,
2246 'expectedusers' => [
2253 'NONE: Filter on last login more than 6 months ago' => (object) [
2254 'accesssince' => ['-6 months'],
2255 'jointype' => filter
::JOINTYPE_NONE
,
2257 'expectedusers' => [
2263 'NONE: Filter on last login more than 3 weeks ago' => (object) [
2264 'accesssince' => ['-3 weeks'],
2265 'jointype' => filter
::JOINTYPE_NONE
,
2267 'expectedusers' => [
2272 'NONE: Filter on last login more than 5 days ago' => (object) [
2273 'accesssince' => ['-5 days'],
2274 'jointype' => filter
::JOINTYPE_NONE
,
2276 'expectedusers' => [
2280 'NONE: Filter on last login more than 2 days ago' => (object) [
2281 'accesssince' => ['-2 days'],
2282 'jointype' => filter
::JOINTYPE_NONE
,
2284 'expectedusers' => [],
2291 foreach ($tests as $testname => $testdata) {
2292 foreach ($testdata->expect
as $expectname => $expectdata) {
2293 $finaltests["{$testname} => {$expectname}"] = [
2294 'users' => $testdata->users
,
2295 'accesssince' => $expectdata->accesssince
,
2296 'jointype' => $expectdata->jointype
,
2297 'count' => $expectdata->count
,
2298 'expectedusers' => $expectdata->expectedusers
,
2307 * Ensure that the joins between filters in the filterset work as expected with the provided test cases.
2309 * @param array $usersdata The list of users to create
2310 * @param array $filterdata The data to filter by
2311 * @param array $groupsavailable The names of groups that should be created in the course
2312 * @param int $jointype The join type to used between each filter being applied
2313 * @param int $count The expected count
2314 * @param array $expectedusers
2315 * @dataProvider filterset_joins_provider
2317 public function test_filterset_joins(array $usersdata, array $filterdata, array $groupsavailable, int $jointype, int $count,
2318 array $expectedusers): void
{
2321 // Ensure sufficient capabilities to view all statuses.
2322 $this->setAdminUser();
2324 // Remove the default role.
2325 set_config('roleid', 0, 'enrol_manual');
2327 $course = $this->getDataGenerator()->create_course();
2328 $coursecontext = context_course
::instance($course->id
);
2329 $roles = $DB->get_records_menu('role', [], '', 'shortname, id');
2332 // Ensure all enrolment methods are enabled (and mapped where required for filtering later).
2333 $enrolinstances = enrol_get_instances($course->id
, false);
2334 $enrolinstancesmap = [];
2335 foreach ($enrolinstances as $instance) {
2336 $plugin = enrol_get_plugin($instance->enrol
);
2337 $plugin->update_status($instance, ENROL_INSTANCE_ENABLED
);
2339 $enrolinstancesmap[$instance->enrol
] = (int) $instance->id
;
2342 // Create the required course groups and mapping.
2343 $nogroupsdata = (object) [
2344 'id' => USERSWITHOUTGROUP
,
2347 $groupsdata = ['nogroups' => $nogroupsdata];
2348 foreach ($groupsavailable as $groupname) {
2350 'courseid' => $course->id
,
2351 'name' => $groupname,
2354 $groupsdata[$groupname] = $this->getDataGenerator()->create_group($groupinfo);
2357 // Create test users.
2358 foreach ($usersdata as $username => $userdata) {
2359 $usertimestamp = empty($userdata['lastlogin']) ?
0 : strtotime($userdata['lastlogin']);
2360 unset($userdata['lastlogin']);
2362 // Prevent randomly generated field values that may cause false fails.
2363 $userdata['firstnamephonetic'] = $userdata['firstnamephonetic'] ??
$userdata['firstname'];
2364 $userdata['lastnamephonetic'] = $userdata['lastnamephonetic'] ??
$userdata['lastname'];
2365 $userdata['middlename'] = $userdata['middlename'] ??
'';
2366 $userdata['alternatename'] = $userdata['alternatename'] ??
$username;
2368 $user = $this->getDataGenerator()->create_user($userdata);
2370 foreach ($userdata['enrolments'] as $details) {
2371 $this->getDataGenerator()->enrol_user($user->id
, $course->id
, $roles[$details['role']],
2372 $details['method'], 0, 0, $details['status']);
2375 foreach ($userdata['groups'] as $groupname) {
2377 'userid' => $user->id
,
2378 'groupid' => (int) $groupsdata[$groupname]->id
,
2380 $this->getDataGenerator()->create_group_member($userinfo);
2383 if ($usertimestamp > 0) {
2384 $this->getDataGenerator()->create_user_course_lastaccess($user, $course, $usertimestamp);
2387 $users[$username] = $user;
2390 // Create a secondary course with users. We should not see these users.
2391 $this->create_course_with_users(10, 10, 10, 10);
2393 // Create the basic filterset.
2394 $filterset = new participants_filterset();
2395 $filterset->set_join_type($jointype);
2396 $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id
]));
2398 // Apply the keywords filter if required.
2399 if (array_key_exists('keywords', $filterdata)) {
2400 $keywordfilter = new string_filter('keywords');
2401 $filterset->add_filter($keywordfilter);
2403 foreach ($filterdata['keywords']['values'] as $keyword) {
2404 $keywordfilter->add_filter_value($keyword);
2406 $keywordfilter->set_join_type($filterdata['keywords']['jointype']);
2409 // Apply enrolment methods filter if required.
2410 if (array_key_exists('enrolmethods', $filterdata)) {
2411 $enrolmethodfilter = new integer_filter('enrolments');
2412 $filterset->add_filter($enrolmethodfilter);
2414 foreach ($filterdata['enrolmethods']['values'] as $enrolmethod) {
2415 $enrolmethodfilter->add_filter_value($enrolinstancesmap[$enrolmethod]);
2417 $enrolmethodfilter->set_join_type($filterdata['enrolmethods']['jointype']);
2420 // Apply roles filter if required.
2421 if (array_key_exists('courseroles', $filterdata)) {
2422 $rolefilter = new integer_filter('roles');
2423 $filterset->add_filter($rolefilter);
2425 foreach ($filterdata['courseroles']['values'] as $rolename) {
2426 $rolefilter->add_filter_value((int) $roles[$rolename]);
2428 $rolefilter->set_join_type($filterdata['courseroles']['jointype']);
2431 // Apply status filter if required.
2432 if (array_key_exists('status', $filterdata)) {
2433 $statusfilter = new integer_filter('status');
2434 $filterset->add_filter($statusfilter);
2436 foreach ($filterdata['status']['values'] as $status) {
2437 $statusfilter->add_filter_value($status);
2439 $statusfilter->set_join_type($filterdata['status']['jointype']);
2442 // Apply groups filter if required.
2443 if (array_key_exists('groups', $filterdata)) {
2444 $groupsfilter = new integer_filter('groups');
2445 $filterset->add_filter($groupsfilter);
2447 foreach ($filterdata['groups']['values'] as $filtergroupname) {
2448 $groupsfilter->add_filter_value((int) $groupsdata[$filtergroupname]->id
);
2450 $groupsfilter->set_join_type($filterdata['groups']['jointype']);
2453 // Apply last access filter if required.
2454 if (array_key_exists('accesssince', $filterdata)) {
2455 $lastaccessfilter = new integer_filter('accesssince');
2456 $filterset->add_filter($lastaccessfilter);
2458 foreach ($filterdata['accesssince']['values'] as $accessstring) {
2459 $lastaccessfilter->add_filter_value(strtotime($accessstring));
2461 $lastaccessfilter->set_join_type($filterdata['accesssince']['jointype']);
2465 $search = new participants_search($course, $coursecontext, $filterset);
2466 $rs = $search->get_participants();
2467 $this->assertInstanceOf(moodle_recordset
::class, $rs);
2468 $records = $this->convert_recordset_to_array($rs);
2470 $this->assertCount($count, $records);
2471 $this->assertEquals($count, $search->get_total_participants_count());
2473 foreach ($expectedusers as $expecteduser) {
2474 $this->assertArrayHasKey($users[$expecteduser]->id
, $records);
2479 * Data provider for filterset join tests.
2483 public function filterset_joins_provider(): array {
2485 // Users with different configurations.
2486 'Users with different configurations' => (object) [
2487 'groupsavailable' => [
2494 'firstname' => 'Adam',
2495 'lastname' => 'Ant',
2498 'role' => 'student',
2499 'method' => 'manual',
2500 'status' => ENROL_USER_ACTIVE
,
2503 'groups' => ['groupa'],
2504 'lastlogin' => '-3 days',
2506 'barbara.bennett' => [
2507 'firstname' => 'Barbara',
2508 'lastname' => 'Bennett',
2511 'role' => 'student',
2512 'method' => 'manual',
2513 'status' => ENROL_USER_ACTIVE
,
2516 'role' => 'teacher',
2517 'method' => 'manual',
2518 'status' => ENROL_USER_ACTIVE
,
2521 'groups' => ['groupb'],
2522 'lastlogin' => '-2 weeks',
2524 'colin.carnforth' => [
2525 'firstname' => 'Colin',
2526 'lastname' => 'Carnforth',
2529 'role' => 'editingteacher',
2531 'status' => ENROL_USER_SUSPENDED
,
2534 'groups' => ['groupa', 'groupb'],
2535 'lastlogin' => '-5 months',
2538 'firstname' => 'Anthony',
2539 'lastname' => 'Rogers',
2542 'role' => 'editingteacher',
2544 'status' => ENROL_USER_SUSPENDED
,
2548 'lastlogin' => '-10 months',
2551 'firstname' => 'Sarah',
2552 'lastname' => 'Rester',
2553 'email' => 'zazu@example.com',
2556 'role' => 'teacher',
2557 'method' => 'manual',
2558 'status' => ENROL_USER_ACTIVE
,
2561 'role' => 'editingteacher',
2563 'status' => ENROL_USER_SUSPENDED
,
2567 'lastlogin' => '-11 months',
2569 'morgan.crikeyson' => [
2570 'firstname' => 'Morgan',
2571 'lastname' => 'Crikeyson',
2574 'role' => 'teacher',
2575 'method' => 'manual',
2576 'status' => ENROL_USER_ACTIVE
,
2579 'groups' => ['groupa'],
2580 'lastlogin' => '-1 week',
2582 'jonathan.bravo' => [
2583 'firstname' => 'Jonathan',
2584 'lastname' => 'Bravo',
2587 'role' => 'student',
2588 'method' => 'manual',
2589 'status' => ENROL_USER_ACTIVE
,
2598 // Tests for jointype: ANY.
2599 'ANY: No filters in filterset' => (object) [
2601 'jointype' => filter
::JOINTYPE_ANY
,
2603 'expectedusers' => [
2613 'ANY: Filterset containing a single filter type' => (object) [
2616 'values' => ['self'],
2617 'jointype' => filter
::JOINTYPE_ANY
,
2620 'jointype' => filter
::JOINTYPE_ANY
,
2622 'expectedusers' => [
2628 'ANY: Filterset matching all filter types on different users' => (object) [
2632 'values' => ['adam'],
2633 'jointype' => filter
::JOINTYPE_ALL
,
2635 // Match Sarah only.
2637 'values' => ['manual', 'self'],
2638 'jointype' => filter
::JOINTYPE_ALL
,
2640 // Match Barbara only.
2642 'values' => ['student', 'teacher'],
2643 'jointype' => filter
::JOINTYPE_ALL
,
2645 // Match Sarah only.
2647 'values' => ['active', 'suspended'],
2648 'jointype' => filter
::JOINTYPE_ALL
,
2650 // Match Colin only.
2652 'values' => ['groupa', 'groupb'],
2653 'jointype' => filter
::JOINTYPE_ALL
,
2655 // Match Jonathan only.
2657 'values' => ['-1 year'],
2658 'jointype' => filter
::JOINTYPE_ALL
,
2661 'jointype' => filter
::JOINTYPE_ANY
,
2663 // Morgan and Tony are not matched, to confirm filtering is not just returning all users.
2664 'expectedusers' => [
2673 // Tests for jointype: ALL.
2674 'ALL: No filters in filterset' => (object) [
2676 'jointype' => filter
::JOINTYPE_ALL
,
2678 'expectedusers' => [
2688 'ALL: Filterset containing a single filter type' => (object) [
2691 'values' => ['self'],
2692 'jointype' => filter
::JOINTYPE_ANY
,
2695 'jointype' => filter
::JOINTYPE_ALL
,
2697 'expectedusers' => [
2703 'ALL: Filterset combining all filter types' => (object) [
2705 // Exclude Adam, Tony, Morgan and Jonathan.
2708 'jointype' => filter
::JOINTYPE_ANY
,
2710 // Exclude Colin and Tony.
2712 'values' => ['manual'],
2713 'jointype' => filter
::JOINTYPE_ANY
,
2715 // Exclude Adam, Barbara and Jonathan.
2717 'values' => ['student'],
2718 'jointype' => filter
::JOINTYPE_NONE
,
2720 // Exclude Colin and Tony.
2722 'values' => ['active'],
2723 'jointype' => filter
::JOINTYPE_ALL
,
2727 'values' => ['groupa', 'nogroups'],
2728 'jointype' => filter
::JOINTYPE_ANY
,
2730 // Exclude Adam, Colin and Barbara.
2732 'values' => ['-6 months'],
2733 'jointype' => filter
::JOINTYPE_ALL
,
2736 'jointype' => filter
::JOINTYPE_ALL
,
2738 'expectedusers' => [
2743 // Tests for jointype: NONE.
2744 'NONE: No filters in filterset' => (object) [
2746 'jointype' => filter
::JOINTYPE_NONE
,
2748 'expectedusers' => [
2758 'NONE: Filterset containing a single filter type' => (object) [
2761 'values' => ['self'],
2762 'jointype' => filter
::JOINTYPE_ANY
,
2765 'jointype' => filter
::JOINTYPE_NONE
,
2767 'expectedusers' => [
2774 'NONE: Filterset combining all filter types' => (object) [
2778 'values' => ['adam'],
2779 'jointype' => filter
::JOINTYPE_ANY
,
2781 // Excludes Colin, Tony and Sarah.
2783 'values' => ['self'],
2784 'jointype' => filter
::JOINTYPE_ANY
,
2786 // Excludes Jonathan.
2788 'values' => ['student'],
2789 'jointype' => filter
::JOINTYPE_NONE
,
2791 // Excludes Colin, Tony and Sarah.
2793 'values' => ['suspended'],
2794 'jointype' => filter
::JOINTYPE_ALL
,
2796 // Excludes Adam, Colin, Tony, Sarah, Morgan and Jonathan.
2798 'values' => ['groupa', 'nogroups'],
2799 'jointype' => filter
::JOINTYPE_ANY
,
2801 // Excludes Tony and Sarah.
2803 'values' => ['-6 months'],
2804 'jointype' => filter
::JOINTYPE_ALL
,
2807 'jointype' => filter
::JOINTYPE_NONE
,
2809 'expectedusers' => [
2818 foreach ($tests as $testname => $testdata) {
2819 foreach ($testdata->expect
as $expectname => $expectdata) {
2820 $finaltests["{$testname} => {$expectname}"] = [
2821 'users' => $testdata->users
,
2822 'filterdata' => $expectdata->filterdata
,
2823 'groupsavailable' => $testdata->groupsavailable
,
2824 'jointype' => $expectdata->jointype
,
2825 'count' => $expectdata->count
,
2826 'expectedusers' => $expectdata->expectedusers
,