on-demand release 4.5dev+
[moodle.git] / user / tests / table / participants_search_test.php
blob9b0ff9cd8645f387316d7fe972813b97c5756e84
1 <?php
2 // This file is part of Moodle - https://moodle.org/
3 //
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.
8 //
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/>.
17 declare(strict_types=1);
19 namespace core_user\table;
21 use advanced_testcase;
22 use context_course;
23 use context_coursecat;
24 use core_table\local\filter\filter;
25 use core_table\local\filter\integer_filter;
26 use core_table\local\filter\string_filter;
27 use core_user\table\participants_filterset;
28 use core_user\table\participants_search;
29 use moodle_recordset;
30 use stdClass;
32 /**
33 * Tests for the implementation of {@link core_user_table_participants_search} class.
35 * @package core_user
36 * @category test
37 * @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
38 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
40 final class participants_search_test extends advanced_testcase {
42 /**
43 * Helper to convert a moodle_recordset to an array of records.
45 * @param moodle_recordset $recordset
46 * @return array
48 protected function convert_recordset_to_array(moodle_recordset $recordset): array {
49 $records = [];
50 foreach ($recordset as $record) {
51 $records[$record->id] = $record;
53 $recordset->close();
55 return $records;
58 /**
59 * Create and enrol a set of users into the specified course.
61 * @param stdClass $course
62 * @param int $count
63 * @param null|string $role
64 * @return array
66 protected function create_and_enrol_users(stdClass $course, int $count, ?string $role = null): array {
67 $this->resetAfterTest(true);
68 $users = [];
70 for ($i = 0; $i < $count; $i++) {
71 $user = $this->getDataGenerator()->create_user();
72 $this->getDataGenerator()->enrol_user($user->id, $course->id, $role);
73 $users[] = $user;
76 return $users;
79 /**
80 * Create a new course with several types of user.
82 * @param int $editingteachers The number of editing teachers to create in the course.
83 * @param int $teachers The number of non-editing teachers to create in the course.
84 * @param int $students The number of students to create in the course.
85 * @param int $norole The number of users with no role to create in the course.
86 * @return stdClass
88 protected function create_course_with_users(int $editingteachers, int $teachers, int $students, int $norole): stdClass {
89 $data = (object) [
90 'course' => $this->getDataGenerator()->create_course(),
91 'editingteachers' => [],
92 'teachers' => [],
93 'students' => [],
94 'norole' => [],
97 $data->context = context_course::instance($data->course->id);
99 $data->editingteachers = $this->create_and_enrol_users($data->course, $editingteachers, 'editingteacher');
100 $data->teachers = $this->create_and_enrol_users($data->course, $teachers, 'teacher');
101 $data->students = $this->create_and_enrol_users($data->course, $students, 'student');
102 $data->norole = $this->create_and_enrol_users($data->course, $norole);
104 return $data;
107 * Ensure that the roles filter works as expected with the provided test cases.
109 * @param array $usersdata The list of users and their roles to create
110 * @param array $testroles The list of roles to filter by
111 * @param int $jointype The join type to use when combining filter values
112 * @param int $count The expected count
113 * @param array $expectedusers
114 * @dataProvider role_provider
116 public function test_roles_filter(array $usersdata, array $testroles, int $jointype, int $count, array $expectedusers): void {
117 global $DB;
119 $roles = $DB->get_records_menu('role', [], '', 'shortname, id');
121 // Remove the default role.
122 set_config('roleid', 0, 'enrol_manual');
124 $course = $this->getDataGenerator()->create_course();
125 $coursecontext = context_course::instance($course->id);
127 $category = $DB->get_record('course_categories', ['id' => $course->category]);
128 $categorycontext = context_coursecat::instance($category->id);
130 $users = [];
132 foreach ($usersdata as $username => $userdata) {
133 $user = $this->getDataGenerator()->create_user(['username' => $username]);
135 if (array_key_exists('courseroles', $userdata)) {
136 $this->getDataGenerator()->enrol_user($user->id, $course->id, null);
137 foreach ($userdata['courseroles'] as $rolename) {
138 $this->getDataGenerator()->role_assign($roles[$rolename], $user->id, $coursecontext->id);
142 if (array_key_exists('categoryroles', $userdata)) {
143 foreach ($userdata['categoryroles'] as $rolename) {
144 $this->getDataGenerator()->role_assign($roles[$rolename], $user->id, $categorycontext->id);
147 $users[$username] = $user;
150 // Create a secondary course with users. We should not see these users.
151 $this->create_course_with_users(1, 1, 1, 1);
153 // Create the basic filter.
154 $filterset = new participants_filterset();
155 $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id]));
157 // Create the role filter.
158 $rolefilter = new integer_filter('roles');
159 $filterset->add_filter($rolefilter);
161 // Configure the filter.
162 foreach ($testroles as $rolename) {
163 $rolefilter->add_filter_value((int) $roles[$rolename]);
165 $rolefilter->set_join_type($jointype);
167 // Run the search.
168 $search = new participants_search($course, $coursecontext, $filterset);
169 $rs = $search->get_participants();
170 $this->assertInstanceOf(moodle_recordset::class, $rs);
171 $records = $this->convert_recordset_to_array($rs);
172 $resetrecords = reset($records);
173 $totalparticipants = $resetrecords->fullcount ?? 0;
175 $this->assertCount($count, $records);
176 $this->assertEquals($count, $totalparticipants);
178 foreach ($expectedusers as $expecteduser) {
179 $this->assertArrayHasKey($users[$expecteduser]->id, $records);
184 * Data provider for role tests.
186 * @return array
188 public static function role_provider(): array {
189 $tests = [
190 // Users who only have one role each.
191 'Users in each role' => (object) [
192 'users' => [
193 'a' => [
194 'courseroles' => [
195 'student',
198 'b' => [
199 'courseroles' => [
200 'student',
203 'c' => [
204 'courseroles' => [
205 'editingteacher',
208 'd' => [
209 'courseroles' => [
210 'editingteacher',
213 'e' => [
214 'courseroles' => [
215 'teacher',
218 'f' => [
219 'courseroles' => [
220 'teacher',
223 // User is enrolled in the course without role.
224 'g' => [
225 'courseroles' => [
229 // User is a category manager and also enrolled without role in the course.
230 'h' => [
231 'courseroles' => [
233 'categoryroles' => [
234 'manager',
238 // User is a category manager and not enrolled in the course.
239 // This user should not show up in any filter.
240 'i' => [
241 'categoryroles' => [
242 'manager',
246 'expect' => [
247 // Tests for jointype: ANY.
248 'ANY: No role filter' => (object) [
249 'roles' => [],
250 'jointype' => filter::JOINTYPE_ANY,
251 'count' => 8,
252 'expectedusers' => [
253 'a',
254 'b',
255 'c',
256 'd',
257 'e',
258 'f',
259 'g',
260 'h',
263 'ANY: Filter on student' => (object) [
264 'roles' => ['student'],
265 'jointype' => filter::JOINTYPE_ANY,
266 'count' => 2,
267 'expectedusers' => [
268 'a',
269 'b',
272 'ANY: Filter on student, teacher' => (object) [
273 'roles' => ['student', 'teacher'],
274 'jointype' => filter::JOINTYPE_ANY,
275 'count' => 4,
276 'expectedusers' => [
277 'a',
278 'b',
279 'e',
280 'f',
283 'ANY: Filter on student, manager (category level role)' => (object) [
284 'roles' => ['student', 'manager'],
285 'jointype' => filter::JOINTYPE_ANY,
286 'count' => 3,
287 'expectedusers' => [
288 'a',
289 'b',
290 'h',
293 'ANY: Filter on student, coursecreator (not assigned)' => (object) [
294 'roles' => ['student', 'coursecreator'],
295 'jointype' => filter::JOINTYPE_ANY,
296 'count' => 2,
297 'expectedusers' => [
298 'a',
299 'b',
303 // Tests for jointype: ALL.
304 'ALL: No role filter' => (object) [
305 'roles' => [],
306 'jointype' => filter::JOINTYPE_ALL,
307 'count' => 8,
308 'expectedusers' => [
309 'a',
310 'b',
311 'c',
312 'd',
313 'e',
314 'f',
315 'g',
316 'h',
319 'ALL: Filter on student' => (object) [
320 'roles' => ['student'],
321 'jointype' => filter::JOINTYPE_ALL,
322 'count' => 2,
323 'expectedusers' => [
324 'a',
325 'b',
328 'ALL: Filter on student, teacher' => (object) [
329 'roles' => ['student', 'teacher'],
330 'jointype' => filter::JOINTYPE_ALL,
331 'count' => 0,
332 'expectedusers' => [],
334 'ALL: Filter on student, manager (category level role))' => (object) [
335 'roles' => ['student', 'manager'],
336 'jointype' => filter::JOINTYPE_ALL,
337 'count' => 0,
338 'expectedusers' => [],
340 'ALL: Filter on student, coursecreator (not assigned))' => (object) [
341 'roles' => ['student', 'coursecreator'],
342 'jointype' => filter::JOINTYPE_ALL,
343 'count' => 0,
344 'expectedusers' => [],
347 // Tests for jointype: NONE.
348 'NONE: No role filter' => (object) [
349 'roles' => [],
350 'jointype' => filter::JOINTYPE_NONE,
351 'count' => 8,
352 'expectedusers' => [
353 'a',
354 'b',
355 'c',
356 'd',
357 'e',
358 'f',
359 'g',
360 'h',
363 'NONE: Filter on student' => (object) [
364 'roles' => ['student'],
365 'jointype' => filter::JOINTYPE_NONE,
366 'count' => 6,
367 'expectedusers' => [
368 'c',
369 'd',
370 'e',
371 'f',
372 'g',
373 'h',
376 'NONE: Filter on student, teacher' => (object) [
377 'roles' => ['student', 'teacher'],
378 'jointype' => filter::JOINTYPE_NONE,
379 'count' => 4,
380 'expectedusers' => [
381 'c',
382 'd',
383 'g',
384 'h',
387 'NONE: Filter on student, manager (category level role))' => (object) [
388 'roles' => ['student', 'manager'],
389 'jointype' => filter::JOINTYPE_NONE,
390 'count' => 5,
391 'expectedusers' => [
392 'c',
393 'd',
394 'e',
395 'f',
396 'g',
399 'NONE: Filter on student, coursecreator (not assigned))' => (object) [
400 'roles' => ['student', 'coursecreator'],
401 'jointype' => filter::JOINTYPE_NONE,
402 'count' => 6,
403 'expectedusers' => [
404 'c',
405 'd',
406 'e',
407 'f',
408 'g',
409 'h',
414 'Users with multiple roles' => (object) [
415 'users' => [
416 'a' => [
417 'courseroles' => [
418 'student',
421 'b' => [
422 'courseroles' => [
423 'student',
424 'teacher',
427 'c' => [
428 'courseroles' => [
429 'editingteacher',
432 'd' => [
433 'courseroles' => [
434 'editingteacher',
437 'e' => [
438 'courseroles' => [
439 'teacher',
440 'editingteacher',
443 'f' => [
444 'courseroles' => [
445 'teacher',
449 // User is enrolled in the course without role.
450 'g' => [
451 'courseroles' => [
455 // User is a category manager and also enrolled without role in the course.
456 'h' => [
457 'courseroles' => [
459 'categoryroles' => [
460 'manager',
464 // User is a category manager and not enrolled in the course.
465 // This user should not show up in any filter.
466 'i' => [
467 'categoryroles' => [
468 'manager',
472 'expect' => [
473 // Tests for jointype: ANY.
474 'ANY: No role filter' => (object) [
475 'roles' => [],
476 'jointype' => filter::JOINTYPE_ANY,
477 'count' => 8,
478 'expectedusers' => [
479 'a',
480 'b',
481 'c',
482 'd',
483 'e',
484 'f',
485 'g',
486 'h',
489 'ANY: Filter on student' => (object) [
490 'roles' => ['student'],
491 'jointype' => filter::JOINTYPE_ANY,
492 'count' => 2,
493 'expectedusers' => [
494 'a',
495 'b',
498 'ANY: Filter on teacher' => (object) [
499 'roles' => ['teacher'],
500 'jointype' => filter::JOINTYPE_ANY,
501 'count' => 3,
502 'expectedusers' => [
503 'b',
504 'e',
505 'f',
508 'ANY: Filter on editingteacher' => (object) [
509 'roles' => ['editingteacher'],
510 'jointype' => filter::JOINTYPE_ANY,
511 'count' => 3,
512 'expectedusers' => [
513 'c',
514 'd',
515 'e',
518 'ANY: Filter on student, teacher' => (object) [
519 'roles' => ['student', 'teacher'],
520 'jointype' => filter::JOINTYPE_ANY,
521 'count' => 4,
522 'expectedusers' => [
523 'a',
524 'b',
525 'e',
526 'f',
529 'ANY: Filter on teacher, editingteacher' => (object) [
530 'roles' => ['teacher', 'editingteacher'],
531 'jointype' => filter::JOINTYPE_ANY,
532 'count' => 5,
533 'expectedusers' => [
534 'b',
535 'c',
536 'd',
537 'e',
538 'f',
541 'ANY: Filter on student, manager (category level role)' => (object) [
542 'roles' => ['student', 'manager'],
543 'jointype' => filter::JOINTYPE_ANY,
544 'count' => 3,
545 'expectedusers' => [
546 'a',
547 'b',
548 'h',
551 'ANY: Filter on student, coursecreator (not assigned)' => (object) [
552 'roles' => ['student', 'coursecreator'],
553 'jointype' => filter::JOINTYPE_ANY,
554 'count' => 2,
555 'expectedusers' => [
556 'a',
557 'b',
561 // Tests for jointype: ALL.
562 'ALL: No role filter' => (object) [
563 'roles' => [],
564 'jointype' => filter::JOINTYPE_ALL,
565 'count' => 8,
566 'expectedusers' => [
567 'a',
568 'b',
569 'c',
570 'd',
571 'e',
572 'f',
573 'g',
574 'h',
577 'ALL: Filter on student' => (object) [
578 'roles' => ['student'],
579 'jointype' => filter::JOINTYPE_ALL,
580 'count' => 2,
581 'expectedusers' => [
582 'a',
583 'b',
586 'ALL: Filter on teacher' => (object) [
587 'roles' => ['teacher'],
588 'jointype' => filter::JOINTYPE_ALL,
589 'count' => 3,
590 'expectedusers' => [
591 'b',
592 'e',
593 'f',
596 'ALL: Filter on editingteacher' => (object) [
597 'roles' => ['editingteacher'],
598 'jointype' => filter::JOINTYPE_ALL,
599 'count' => 3,
600 'expectedusers' => [
601 'c',
602 'd',
603 'e',
606 'ALL: Filter on student, teacher' => (object) [
607 'roles' => ['student', 'teacher'],
608 'jointype' => filter::JOINTYPE_ALL,
609 'count' => 1,
610 'expectedusers' => [
611 'b',
614 'ALL: Filter on teacher, editingteacher' => (object) [
615 'roles' => ['teacher', 'editingteacher'],
616 'jointype' => filter::JOINTYPE_ALL,
617 'count' => 1,
618 'expectedusers' => [
619 'e',
622 'ALL: Filter on student, manager (category level role)' => (object) [
623 'roles' => ['student', 'manager'],
624 'jointype' => filter::JOINTYPE_ALL,
625 'count' => 0,
626 'expectedusers' => [],
628 'ALL: Filter on student, coursecreator (not assigned)' => (object) [
629 'roles' => ['student', 'coursecreator'],
630 'jointype' => filter::JOINTYPE_ALL,
631 'count' => 0,
632 'expectedusers' => [],
635 // Tests for jointype: NONE.
636 'NONE: No role filter' => (object) [
637 'roles' => [],
638 'jointype' => filter::JOINTYPE_NONE,
639 'count' => 8,
640 'expectedusers' => [
641 'a',
642 'b',
643 'c',
644 'd',
645 'e',
646 'f',
647 'g',
648 'h',
651 'NONE: Filter on student' => (object) [
652 'roles' => ['student'],
653 'jointype' => filter::JOINTYPE_NONE,
654 'count' => 6,
655 'expectedusers' => [
656 'c',
657 'd',
658 'e',
659 'f',
660 'g',
661 'h',
664 'NONE: Filter on teacher' => (object) [
665 'roles' => ['teacher'],
666 'jointype' => filter::JOINTYPE_NONE,
667 'count' => 5,
668 'expectedusers' => [
669 'a',
670 'c',
671 'd',
672 'g',
673 'h',
676 'NONE: Filter on editingteacher' => (object) [
677 'roles' => ['editingteacher'],
678 'jointype' => filter::JOINTYPE_NONE,
679 'count' => 5,
680 'expectedusers' => [
681 'a',
682 'b',
683 'f',
684 'g',
685 'h',
688 'NONE: Filter on student, teacher' => (object) [
689 'roles' => ['student', 'teacher'],
690 'jointype' => filter::JOINTYPE_NONE,
691 'count' => 4,
692 'expectedusers' => [
693 'c',
694 'd',
695 'g',
696 'h',
699 'NONE: Filter on teacher, editingteacher' => (object) [
700 'roles' => ['teacher', 'editingteacher'],
701 'jointype' => filter::JOINTYPE_NONE,
702 'count' => 3,
703 'expectedusers' => [
704 'a',
705 'g',
706 'h',
709 'NONE: Filter on student, manager (category level role)' => (object) [
710 'roles' => ['student', 'manager'],
711 'jointype' => filter::JOINTYPE_NONE,
712 'count' => 5,
713 'expectedusers' => [
714 'c',
715 'd',
716 'e',
717 'f',
718 'g',
721 'NONE: Filter on student, coursecreator (not assigned)' => (object) [
722 'roles' => ['student', 'coursecreator'],
723 'jointype' => filter::JOINTYPE_NONE,
724 'count' => 6,
725 'expectedusers' => [
726 'c',
727 'd',
728 'e',
729 'f',
730 'g',
731 'h',
738 $finaltests = [];
739 foreach ($tests as $testname => $testdata) {
740 foreach ($testdata->expect as $expectname => $expectdata) {
741 $finaltests["{$testname} => {$expectname}"] = [
742 'users' => $testdata->users,
743 'roles' => $expectdata->roles,
744 'jointype' => $expectdata->jointype,
745 'count' => $expectdata->count,
746 'expectedusers' => $expectdata->expectedusers,
751 return $finaltests;
755 * Test participant search country filter
757 * @param array $usersdata
758 * @param array $countries
759 * @param int $jointype
760 * @param array $expectedusers
762 * @dataProvider country_provider
764 public function test_country_filter(array $usersdata, array $countries, int $jointype, array $expectedusers): void {
765 $this->resetAfterTest();
767 $course = $this->getDataGenerator()->create_course();
768 $users = [];
770 foreach ($usersdata as $username => $country) {
771 $users[$username] = $this->getDataGenerator()->create_and_enrol($course, 'student', (object) [
772 'username' => $username,
773 'country' => $country,
777 // Add filters (courseid is required).
778 $filterset = new participants_filterset();
779 $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id]));
780 $filterset->add_filter(new string_filter('country', $jointype, $countries));
782 // Run the search, assert count matches the number of expected users.
783 $search = new participants_search($course, context_course::instance($course->id), $filterset);
784 $rs = $search->get_participants();
785 $totalparticipants = $rs->current()->fullcount ?? 0;
786 $this->assertEquals(count($expectedusers), $totalparticipants);
788 $this->assertInstanceOf(moodle_recordset::class, $rs);
790 // Assert that each expected user is within the participant records.
791 $records = $this->convert_recordset_to_array($rs);
792 foreach ($expectedusers as $expecteduser) {
793 $this->assertArrayHasKey($users[$expecteduser]->id, $records);
798 * Data provider for {@see test_country_filter}
800 * @return array
802 public function country_provider(): array {
803 $tests = [
804 'users' => [
805 'user1' => 'DE',
806 'user2' => 'ES',
807 'user3' => 'ES',
808 'user4' => 'GB',
810 'expects' => [
811 // Tests for jointype: ANY.
812 'ANY: No filter' => (object) [
813 'countries' => [],
814 'jointype' => filter::JOINTYPE_ANY,
815 'expectedusers' => [
816 'user1',
817 'user2',
818 'user3',
819 'user4',
822 'ANY: Matching filters' => (object) [
823 'countries' => [
824 'DE',
825 'GB',
827 'jointype' => filter::JOINTYPE_ANY,
828 'expectedusers' => [
829 'user1',
830 'user4',
833 'ANY: Non-matching filters' => (object) [
834 'countries' => [
835 'RU',
837 'jointype' => filter::JOINTYPE_ANY,
838 'expectedusers' => [],
841 // Tests for jointype: ALL.
842 'ALL: No filter' => (object) [
843 'countries' => [],
844 'jointype' => filter::JOINTYPE_ALL,
845 'expectedusers' => [
846 'user1',
847 'user2',
848 'user3',
849 'user4',
852 'ALL: Matching filters' => (object) [
853 'countries' => [
854 'DE',
855 'GB',
857 'jointype' => filter::JOINTYPE_ALL,
858 'expectedusers' => [
859 'user1',
860 'user4',
863 'ALL: Non-matching filters' => (object) [
864 'countries' => [
865 'RU',
867 'jointype' => filter::JOINTYPE_ALL,
868 'expectedusers' => [],
871 // Tests for jointype: NONE.
872 'NONE: No filter' => (object) [
873 'countries' => [],
874 'jointype' => filter::JOINTYPE_NONE,
875 'expectedusers' => [
876 'user1',
877 'user2',
878 'user3',
879 'user4',
882 'NONE: Matching filters' => (object) [
883 'countries' => [
884 'DE',
885 'GB',
887 'jointype' => filter::JOINTYPE_NONE,
888 'expectedusers' => [
889 'user2',
890 'user3',
893 'NONE: Non-matching filters' => (object) [
894 'countries' => [
895 'RU',
897 'jointype' => filter::JOINTYPE_NONE,
898 'expectedusers' => [
899 'user1',
900 'user2',
901 'user3',
902 'user4',
908 $finaltests = [];
909 foreach ($tests['expects'] as $testname => $test) {
910 $finaltests[$testname] = [
911 'users' => $tests['users'],
912 'countries' => $test->countries,
913 'jointype' => $test->jointype,
914 'expectedusers' => $test->expectedusers,
918 return $finaltests;
922 * Ensure that the keywords filter works as expected with the provided test cases.
924 * @param array $usersdata The list of users to create
925 * @param array $keywords The list of keywords to filter by
926 * @param int $jointype The join type to use when combining filter values
927 * @param int $count The expected count
928 * @param array $expectedusers
929 * @param string $asuser If non-blank, uses that user account (for identify field permission checks)
930 * @dataProvider keywords_provider
932 public function test_keywords_filter(array $usersdata, array $keywords, int $jointype, int $count,
933 array $expectedusers, string $asuser): void {
934 global $DB;
936 $course = $this->getDataGenerator()->create_course();
937 $coursecontext = context_course::instance($course->id);
938 $users = [];
940 // Create the custom user profile field and put it into showuseridentity.
941 $this->getDataGenerator()->create_custom_profile_field(
942 ['datatype' => 'text', 'shortname' => 'frog', 'name' => 'Fave frog']);
943 set_config('showuseridentity', 'email,profile_field_frog');
945 foreach ($usersdata as $username => $userdata) {
946 // Prevent randomly generated field values that may cause false fails.
947 $userdata['firstnamephonetic'] = $userdata['firstnamephonetic'] ?? $userdata['firstname'];
948 $userdata['lastnamephonetic'] = $userdata['lastnamephonetic'] ?? $userdata['lastname'];
949 $userdata['middlename'] = $userdata['middlename'] ?? '';
950 $userdata['alternatename'] = $userdata['alternatename'] ?? $username;
952 $user = $this->getDataGenerator()->create_user($userdata);
953 $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
954 $users[$username] = $user;
957 // Create a secondary course with users. We should not see these users.
958 $this->create_course_with_users(10, 10, 10, 10);
960 // Create the basic filter.
961 $filterset = new participants_filterset();
962 $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id]));
964 // Create the keyword filter.
965 $keywordfilter = new string_filter('keywords');
966 $filterset->add_filter($keywordfilter);
968 // Configure the filter.
969 foreach ($keywords as $keyword) {
970 $keywordfilter->add_filter_value($keyword);
972 $keywordfilter->set_join_type($jointype);
974 if ($asuser) {
975 $this->setUser($DB->get_record('user', ['username' => $asuser]));
978 // Run the search.
979 $search = new participants_search($course, $coursecontext, $filterset);
980 $rs = $search->get_participants();
981 $this->assertInstanceOf(moodle_recordset::class, $rs);
982 $records = $this->convert_recordset_to_array($rs);
983 $resetrecords = reset($records);
984 $totalparticipants = $resetrecords->fullcount ?? 0;
986 $this->assertCount($count, $records);
987 $this->assertEquals($count, $totalparticipants);
989 foreach ($expectedusers as $expecteduser) {
990 $this->assertArrayHasKey($users[$expecteduser]->id, $records);
995 * Data provider for keywords tests.
997 * @return array
999 public function keywords_provider(): array {
1000 $tests = [
1001 // Users where the keyword matches basic user fields such as names and email.
1002 'Users with basic names' => (object) [
1003 'users' => [
1004 'adam.ant' => [
1005 'firstname' => 'Adam',
1006 'lastname' => 'Ant',
1008 'barbara.bennett' => [
1009 'firstname' => 'Barbara',
1010 'lastname' => 'Bennett',
1011 'alternatename' => 'Babs',
1012 'firstnamephonetic' => 'Barbra',
1013 'lastnamephonetic' => 'Benit',
1014 'profile_field_frog' => 'Kermit',
1016 'colin.carnforth' => [
1017 'firstname' => 'Colin',
1018 'lastname' => 'Carnforth',
1019 'middlename' => 'Jeffery',
1021 'tony.rogers' => [
1022 'firstname' => 'Anthony',
1023 'lastname' => 'Rogers',
1024 'lastnamephonetic' => 'Rowjours',
1025 'profile_field_frog' => 'Mr Toad',
1027 'sarah.rester' => [
1028 'firstname' => 'Sarah',
1029 'lastname' => 'Rester',
1030 'email' => 'zazu@example.com',
1031 'firstnamephonetic' => 'Sera',
1034 'expect' => [
1035 // Tests for jointype: ANY.
1036 'ANY: No filter' => (object) [
1037 'keywords' => [],
1038 'jointype' => filter::JOINTYPE_ANY,
1039 'count' => 5,
1040 'expectedusers' => [
1041 'adam.ant',
1042 'barbara.bennett',
1043 'colin.carnforth',
1044 'tony.rogers',
1045 'sarah.rester',
1048 'ANY: Filter on first name only' => (object) [
1049 'keywords' => ['adam'],
1050 'jointype' => filter::JOINTYPE_ANY,
1051 'count' => 1,
1052 'expectedusers' => [
1053 'adam.ant',
1056 'ANY: Filter on last name only' => (object) [
1057 'keywords' => ['BeNNeTt'],
1058 'jointype' => filter::JOINTYPE_ANY,
1059 'count' => 1,
1060 'expectedusers' => [
1061 'barbara.bennett',
1064 'ANY: Filter on first/Last name' => (object) [
1065 'keywords' => ['ant'],
1066 'jointype' => filter::JOINTYPE_ANY,
1067 'count' => 2,
1068 'expectedusers' => [
1069 'adam.ant',
1070 'tony.rogers',
1073 'ANY: Filter on fullname only' => (object) [
1074 'keywords' => ['Barbara Bennett'],
1075 'jointype' => filter::JOINTYPE_ANY,
1076 'count' => 1,
1077 'expectedusers' => [
1078 'barbara.bennett',
1081 'ANY: Filter on middlename only' => (object) [
1082 'keywords' => ['Jeff'],
1083 'jointype' => filter::JOINTYPE_ANY,
1084 'count' => 1,
1085 'expectedusers' => [
1086 'colin.carnforth',
1089 'ANY: Filter on username (no match)' => (object) [
1090 'keywords' => ['sara.rester'],
1091 'jointype' => filter::JOINTYPE_ANY,
1092 'count' => 0,
1093 'expectedusers' => [],
1095 'ANY: Filter on email only' => (object) [
1096 'keywords' => ['zazu'],
1097 'jointype' => filter::JOINTYPE_ANY,
1098 'count' => 1,
1099 'expectedusers' => [
1100 'sarah.rester',
1103 'ANY: Filter on first name phonetic only' => (object) [
1104 'keywords' => ['Sera'],
1105 'jointype' => filter::JOINTYPE_ANY,
1106 'count' => 1,
1107 'expectedusers' => [
1108 'sarah.rester',
1111 'ANY: Filter on last name phonetic only' => (object) [
1112 'keywords' => ['jour'],
1113 'jointype' => filter::JOINTYPE_ANY,
1114 'count' => 1,
1115 'expectedusers' => [
1116 'tony.rogers',
1119 'ANY: Filter on alternate name only' => (object) [
1120 'keywords' => ['Babs'],
1121 'jointype' => filter::JOINTYPE_ANY,
1122 'count' => 1,
1123 'expectedusers' => [
1124 'barbara.bennett',
1127 'ANY: Filter on multiple keywords (first/middle/last name)' => (object) [
1128 'keywords' => ['ant', 'Jeff', 'rog'],
1129 'jointype' => filter::JOINTYPE_ANY,
1130 'count' => 3,
1131 'expectedusers' => [
1132 'adam.ant',
1133 'colin.carnforth',
1134 'tony.rogers',
1137 'ANY: Filter on multiple keywords (phonetic/alternate names)' => (object) [
1138 'keywords' => ['era', 'Bab', 'ours'],
1139 'jointype' => filter::JOINTYPE_ANY,
1140 'count' => 3,
1141 'expectedusers' => [
1142 'barbara.bennett',
1143 'sarah.rester',
1144 'tony.rogers',
1147 'ANY: Filter on custom profile field' => (object) [
1148 'keywords' => ['Kermit', 'Mr Toad'],
1149 'jointype' => filter::JOINTYPE_ANY,
1150 'count' => 2,
1151 'expectedusers' => [
1152 'barbara.bennett',
1153 'tony.rogers',
1155 'asuser' => 'admin'
1157 'ANY: Filter on custom profile field (no permissions)' => (object) [
1158 'keywords' => ['Kermit', 'Mr Toad'],
1159 'jointype' => filter::JOINTYPE_ANY,
1160 'count' => 0,
1161 'expectedusers' => [],
1162 'asuser' => 'barbara.bennett'
1165 // Tests for jointype: ALL.
1166 'ALL: No filter' => (object) [
1167 'keywords' => [],
1168 'jointype' => filter::JOINTYPE_ALL,
1169 'count' => 5,
1170 'expectedusers' => [
1171 'adam.ant',
1172 'barbara.bennett',
1173 'colin.carnforth',
1174 'tony.rogers',
1175 'sarah.rester',
1178 'ALL: Filter on first name only' => (object) [
1179 'keywords' => ['adam'],
1180 'jointype' => filter::JOINTYPE_ALL,
1181 'count' => 1,
1182 'expectedusers' => [
1183 'adam.ant',
1186 'ALL: Filter on last name only' => (object) [
1187 'keywords' => ['BeNNeTt'],
1188 'jointype' => filter::JOINTYPE_ALL,
1189 'count' => 1,
1190 'expectedusers' => [
1191 'barbara.bennett',
1194 'ALL: Filter on first/Last name' => (object) [
1195 'keywords' => ['ant'],
1196 'jointype' => filter::JOINTYPE_ALL,
1197 'count' => 2,
1198 'expectedusers' => [
1199 'adam.ant',
1200 'tony.rogers',
1203 'ALL: Filter on middlename only' => (object) [
1204 'keywords' => ['Jeff'],
1205 'jointype' => filter::JOINTYPE_ALL,
1206 'count' => 1,
1207 'expectedusers' => [
1208 'colin.carnforth',
1211 'ALL: Filter on username (no match)' => (object) [
1212 'keywords' => ['sara.rester'],
1213 'jointype' => filter::JOINTYPE_ALL,
1214 'count' => 0,
1215 'expectedusers' => [],
1217 'ALL: Filter on email only' => (object) [
1218 'keywords' => ['zazu'],
1219 'jointype' => filter::JOINTYPE_ALL,
1220 'count' => 1,
1221 'expectedusers' => [
1222 'sarah.rester',
1225 'ALL: Filter on first name phonetic only' => (object) [
1226 'keywords' => ['Sera'],
1227 'jointype' => filter::JOINTYPE_ALL,
1228 'count' => 1,
1229 'expectedusers' => [
1230 'sarah.rester',
1233 'ALL: Filter on last name phonetic only' => (object) [
1234 'keywords' => ['jour'],
1235 'jointype' => filter::JOINTYPE_ALL,
1236 'count' => 1,
1237 'expectedusers' => [
1238 'tony.rogers',
1241 'ALL: Filter on alternate name only' => (object) [
1242 'keywords' => ['Babs'],
1243 'jointype' => filter::JOINTYPE_ALL,
1244 'count' => 1,
1245 'expectedusers' => [
1246 'barbara.bennett',
1249 'ALL: Filter on multiple keywords (first/last name)' => (object) [
1250 'keywords' => ['ant', 'rog'],
1251 'jointype' => filter::JOINTYPE_ALL,
1252 'count' => 1,
1253 'expectedusers' => [
1254 'tony.rogers',
1257 'ALL: Filter on multiple keywords (first/middle/last name)' => (object) [
1258 'keywords' => ['ant', 'Jeff', 'rog'],
1259 'jointype' => filter::JOINTYPE_ALL,
1260 'count' => 0,
1261 'expectedusers' => [],
1263 'ALL: Filter on multiple keywords (phonetic/alternate names)' => (object) [
1264 'keywords' => ['Bab', 'bra', 'nit'],
1265 'jointype' => filter::JOINTYPE_ALL,
1266 'count' => 1,
1267 'expectedusers' => [
1268 'barbara.bennett',
1271 'ALL: Filter on custom profile field' => (object) [
1272 'keywords' => ['Kermit', 'Kermi'],
1273 'jointype' => filter::JOINTYPE_ALL,
1274 'count' => 1,
1275 'expectedusers' => [
1276 'barbara.bennett',
1278 'asuser' => 'admin',
1280 'ALL: Filter on custom profile field (no permissions)' => (object) [
1281 'keywords' => ['Kermit', 'Kermi'],
1282 'jointype' => filter::JOINTYPE_ALL,
1283 'count' => 0,
1284 'expectedusers' => [],
1285 'asuser' => 'barbara.bennett',
1288 // Tests for jointype: NONE.
1289 'NONE: No filter' => (object) [
1290 'keywords' => [],
1291 'jointype' => filter::JOINTYPE_NONE,
1292 'count' => 5,
1293 'expectedusers' => [
1294 'adam.ant',
1295 'barbara.bennett',
1296 'colin.carnforth',
1297 'tony.rogers',
1298 'sarah.rester',
1301 'NONE: Filter on first name only' => (object) [
1302 'keywords' => ['ara'],
1303 'jointype' => filter::JOINTYPE_NONE,
1304 'count' => 3,
1305 'expectedusers' => [
1306 'adam.ant',
1307 'colin.carnforth',
1308 'tony.rogers',
1311 'NONE: Filter on last name only' => (object) [
1312 'keywords' => ['BeNNeTt'],
1313 'jointype' => filter::JOINTYPE_NONE,
1314 'count' => 4,
1315 'expectedusers' => [
1316 'adam.ant',
1317 'colin.carnforth',
1318 'tony.rogers',
1319 'sarah.rester',
1322 'NONE: Filter on first/Last name' => (object) [
1323 'keywords' => ['ar'],
1324 'jointype' => filter::JOINTYPE_NONE,
1325 'count' => 2,
1326 'expectedusers' => [
1327 'adam.ant',
1328 'tony.rogers',
1331 'NONE: Filter on middlename only' => (object) [
1332 'keywords' => ['Jeff'],
1333 'jointype' => filter::JOINTYPE_NONE,
1334 'count' => 4,
1335 'expectedusers' => [
1336 'adam.ant',
1337 'barbara.bennett',
1338 'tony.rogers',
1339 'sarah.rester',
1342 'NONE: Filter on username (no match)' => (object) [
1343 'keywords' => ['sara.rester'],
1344 'jointype' => filter::JOINTYPE_NONE,
1345 'count' => 5,
1346 'expectedusers' => [
1347 'adam.ant',
1348 'barbara.bennett',
1349 'colin.carnforth',
1350 'tony.rogers',
1351 'sarah.rester',
1354 'NONE: Filter on email' => (object) [
1355 'keywords' => ['zazu'],
1356 'jointype' => filter::JOINTYPE_NONE,
1357 'count' => 4,
1358 'expectedusers' => [
1359 'adam.ant',
1360 'barbara.bennett',
1361 'colin.carnforth',
1362 'tony.rogers',
1365 'NONE: Filter on first name phonetic only' => (object) [
1366 'keywords' => ['Sera'],
1367 'jointype' => filter::JOINTYPE_NONE,
1368 'count' => 4,
1369 'expectedusers' => [
1370 'adam.ant',
1371 'barbara.bennett',
1372 'colin.carnforth',
1373 'tony.rogers',
1376 'NONE: Filter on last name phonetic only' => (object) [
1377 'keywords' => ['jour'],
1378 'jointype' => filter::JOINTYPE_NONE,
1379 'count' => 4,
1380 'expectedusers' => [
1381 'adam.ant',
1382 'barbara.bennett',
1383 'colin.carnforth',
1384 'sarah.rester',
1387 'NONE: Filter on alternate name only' => (object) [
1388 'keywords' => ['Babs'],
1389 'jointype' => filter::JOINTYPE_NONE,
1390 'count' => 4,
1391 'expectedusers' => [
1392 'adam.ant',
1393 'colin.carnforth',
1394 'tony.rogers',
1395 'sarah.rester',
1398 'NONE: Filter on multiple keywords (first/last name)' => (object) [
1399 'keywords' => ['ara', 'rog'],
1400 'jointype' => filter::JOINTYPE_NONE,
1401 'count' => 2,
1402 'expectedusers' => [
1403 'adam.ant',
1404 'colin.carnforth',
1407 'NONE: Filter on multiple keywords (first/middle/last name)' => (object) [
1408 'keywords' => ['ant', 'Jeff', 'rog'],
1409 'jointype' => filter::JOINTYPE_NONE,
1410 'count' => 2,
1411 'expectedusers' => [
1412 'barbara.bennett',
1413 'sarah.rester',
1416 'NONE: Filter on multiple keywords (phonetic/alternate names)' => (object) [
1417 'keywords' => ['Bab', 'bra', 'nit'],
1418 'jointype' => filter::JOINTYPE_NONE,
1419 'count' => 4,
1420 'expectedusers' => [
1421 'adam.ant',
1422 'colin.carnforth',
1423 'tony.rogers',
1424 'sarah.rester',
1427 'NONE: Filter on custom profile field' => (object) [
1428 'keywords' => ['Kermit', 'Mr Toad'],
1429 'jointype' => filter::JOINTYPE_NONE,
1430 'count' => 3,
1431 'expectedusers' => [
1432 'adam.ant',
1433 'colin.carnforth',
1434 'sarah.rester',
1436 'asuser' => 'admin',
1438 'NONE: Filter on custom profile field (no permissions)' => (object) [
1439 'keywords' => ['Kermit', 'Mr Toad'],
1440 'jointype' => filter::JOINTYPE_NONE,
1441 'count' => 5,
1442 'expectedusers' => [
1443 'adam.ant',
1444 'barbara.bennett',
1445 'colin.carnforth',
1446 'tony.rogers',
1447 'sarah.rester',
1449 'asuser' => 'barbara.bennett',
1455 $finaltests = [];
1456 foreach ($tests as $testname => $testdata) {
1457 foreach ($testdata->expect as $expectname => $expectdata) {
1458 $finaltests["{$testname} => {$expectname}"] = [
1459 'users' => $testdata->users,
1460 'keywords' => $expectdata->keywords,
1461 'jointype' => $expectdata->jointype,
1462 'count' => $expectdata->count,
1463 'expectedusers' => $expectdata->expectedusers,
1464 'asuser' => $expectdata->asuser ?? ''
1469 return $finaltests;
1473 * Ensure that the enrolment status filter works as expected with the provided test cases.
1475 * @param array $usersdata The list of users to create
1476 * @param array $statuses The list of statuses to filter by
1477 * @param int $jointype The join type to use when combining filter values
1478 * @param int $count The expected count
1479 * @param array $expectedusers
1480 * @dataProvider status_provider
1482 public function test_status_filter(array $usersdata, array $statuses, int $jointype, int $count, array $expectedusers): void {
1483 $course = $this->getDataGenerator()->create_course();
1484 $coursecontext = context_course::instance($course->id);
1485 $users = [];
1487 // Ensure sufficient capabilities to view all statuses.
1488 $this->setAdminUser();
1490 // Ensure all enrolment methods enabled.
1491 $enrolinstances = enrol_get_instances($course->id, false);
1492 foreach ($enrolinstances as $instance) {
1493 $plugin = enrol_get_plugin($instance->enrol);
1494 $plugin->update_status($instance, ENROL_INSTANCE_ENABLED);
1497 foreach ($usersdata as $username => $userdata) {
1498 $user = $this->getDataGenerator()->create_user(['username' => $username]);
1500 if (array_key_exists('status', $userdata)) {
1501 foreach ($userdata['status'] as $enrolmethod => $status) {
1502 $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student', $enrolmethod, 0, 0, $status);
1506 $users[$username] = $user;
1509 // Create a secondary course with users. We should not see these users.
1510 $this->create_course_with_users(1, 1, 1, 1);
1512 // Create the basic filter.
1513 $filterset = new participants_filterset();
1514 $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id]));
1516 // Create the status filter.
1517 $statusfilter = new integer_filter('status');
1518 $filterset->add_filter($statusfilter);
1520 // Configure the filter.
1521 foreach ($statuses as $status) {
1522 $statusfilter->add_filter_value($status);
1524 $statusfilter->set_join_type($jointype);
1526 // Run the search.
1527 $search = new participants_search($course, $coursecontext, $filterset);
1528 $rs = $search->get_participants();
1529 $this->assertInstanceOf(moodle_recordset::class, $rs);
1530 $records = $this->convert_recordset_to_array($rs);
1531 $resetrecords = reset($records);
1532 $totalparticipants = $resetrecords->fullcount ?? 0;
1534 $this->assertCount($count, $records);
1535 $this->assertEquals($count, $totalparticipants);
1537 foreach ($expectedusers as $expecteduser) {
1538 $this->assertArrayHasKey($users[$expecteduser]->id, $records);
1543 * Data provider for status filter tests.
1545 * @return array
1547 public function status_provider(): array {
1548 $tests = [
1549 // Users with different statuses and enrolment methods (so multiple statuses are possible for the same user).
1550 'Users with different enrolment statuses' => (object) [
1551 'users' => [
1552 'a' => [
1553 'status' => [
1554 'manual' => ENROL_USER_ACTIVE,
1557 'b' => [
1558 'status' => [
1559 'self' => ENROL_USER_ACTIVE,
1562 'c' => [
1563 'status' => [
1564 'manual' => ENROL_USER_SUSPENDED,
1567 'd' => [
1568 'status' => [
1569 'self' => ENROL_USER_SUSPENDED,
1572 'e' => [
1573 'status' => [
1574 'manual' => ENROL_USER_ACTIVE,
1575 'self' => ENROL_USER_SUSPENDED,
1579 'expect' => [
1580 // Tests for jointype: ANY.
1581 'ANY: No filter' => (object) [
1582 'status' => [],
1583 'jointype' => filter::JOINTYPE_ANY,
1584 'count' => 5,
1585 'expectedusers' => [
1586 'a',
1587 'b',
1588 'c',
1589 'd',
1590 'e',
1593 'ANY: Filter on active only' => (object) [
1594 'status' => [ENROL_USER_ACTIVE],
1595 'jointype' => filter::JOINTYPE_ANY,
1596 'count' => 3,
1597 'expectedusers' => [
1598 'a',
1599 'b',
1600 'e',
1603 'ANY: Filter on suspended only' => (object) [
1604 'status' => [ENROL_USER_SUSPENDED],
1605 'jointype' => filter::JOINTYPE_ANY,
1606 'count' => 3,
1607 'expectedusers' => [
1608 'c',
1609 'd',
1610 'e',
1613 'ANY: Filter on multiple statuses' => (object) [
1614 'status' => [ENROL_USER_ACTIVE, ENROL_USER_SUSPENDED],
1615 'jointype' => filter::JOINTYPE_ANY,
1616 'count' => 5,
1617 'expectedusers' => [
1618 'a',
1619 'b',
1620 'c',
1621 'd',
1622 'e',
1626 // Tests for jointype: ALL.
1627 'ALL: No filter' => (object) [
1628 'status' => [],
1629 'jointype' => filter::JOINTYPE_ALL,
1630 'count' => 5,
1631 'expectedusers' => [
1632 'a',
1633 'b',
1634 'c',
1635 'd',
1636 'e',
1639 'ALL: Filter on active only' => (object) [
1640 'status' => [ENROL_USER_ACTIVE],
1641 'jointype' => filter::JOINTYPE_ALL,
1642 'count' => 3,
1643 'expectedusers' => [
1644 'a',
1645 'b',
1646 'e',
1649 'ALL: Filter on suspended only' => (object) [
1650 'status' => [ENROL_USER_SUSPENDED],
1651 'jointype' => filter::JOINTYPE_ALL,
1652 'count' => 3,
1653 'expectedusers' => [
1654 'c',
1655 'd',
1656 'e',
1659 'ALL: Filter on multiple statuses' => (object) [
1660 'status' => [ENROL_USER_ACTIVE, ENROL_USER_SUSPENDED],
1661 'jointype' => filter::JOINTYPE_ALL,
1662 'count' => 1,
1663 'expectedusers' => [
1664 'e',
1668 // Tests for jointype: NONE.
1669 'NONE: No filter' => (object) [
1670 'status' => [],
1671 'jointype' => filter::JOINTYPE_NONE,
1672 'count' => 5,
1673 'expectedusers' => [
1674 'a',
1675 'b',
1676 'c',
1677 'd',
1678 'e',
1681 'NONE: Filter on active only' => (object) [
1682 'status' => [ENROL_USER_ACTIVE],
1683 'jointype' => filter::JOINTYPE_NONE,
1684 'count' => 3,
1685 'expectedusers' => [
1686 'c',
1687 'd',
1688 'e',
1691 'NONE: Filter on suspended only' => (object) [
1692 'status' => [ENROL_USER_SUSPENDED],
1693 'jointype' => filter::JOINTYPE_NONE,
1694 'count' => 3,
1695 'expectedusers' => [
1696 'a',
1697 'b',
1698 'e',
1701 'NONE: Filter on multiple statuses' => (object) [
1702 'status' => [ENROL_USER_ACTIVE, ENROL_USER_SUSPENDED],
1703 'jointype' => filter::JOINTYPE_NONE,
1704 'count' => 0,
1705 'expectedusers' => [],
1711 $finaltests = [];
1712 foreach ($tests as $testname => $testdata) {
1713 foreach ($testdata->expect as $expectname => $expectdata) {
1714 $finaltests["{$testname} => {$expectname}"] = [
1715 'users' => $testdata->users,
1716 'status' => $expectdata->status,
1717 'jointype' => $expectdata->jointype,
1718 'count' => $expectdata->count,
1719 'expectedusers' => $expectdata->expectedusers,
1724 return $finaltests;
1728 * Ensure that the enrolment methods filter works as expected with the provided test cases.
1730 * @param array $usersdata The list of users to create
1731 * @param array $enrolmethods The list of enrolment methods to filter by
1732 * @param int $jointype The join type to use when combining filter values
1733 * @param int $count The expected count
1734 * @param array $expectedusers
1735 * @dataProvider enrolments_provider
1737 public function test_enrolments_filter(array $usersdata, array $enrolmethods, int $jointype, int $count,
1738 array $expectedusers): void {
1740 $course = $this->getDataGenerator()->create_course();
1741 $coursecontext = context_course::instance($course->id);
1742 $users = [];
1744 // Ensure all enrolment methods enabled and mapped for setting the filter later.
1745 $enrolinstances = enrol_get_instances($course->id, false);
1746 $enrolinstancesmap = [];
1747 foreach ($enrolinstances as $instance) {
1748 $plugin = enrol_get_plugin($instance->enrol);
1749 $plugin->update_status($instance, ENROL_INSTANCE_ENABLED);
1751 $enrolinstancesmap[$instance->enrol] = (int) $instance->id;
1754 foreach ($usersdata as $username => $userdata) {
1755 $user = $this->getDataGenerator()->create_user(['username' => $username]);
1757 if (array_key_exists('enrolmethods', $userdata)) {
1758 foreach ($userdata['enrolmethods'] as $enrolmethod) {
1759 $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student', $enrolmethod);
1763 $users[$username] = $user;
1766 // Create a secondary course with users. We should not see these users.
1767 $this->create_course_with_users(1, 1, 1, 1);
1769 // Create the basic filter.
1770 $filterset = new participants_filterset();
1771 $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id]));
1773 // Create the enrolment methods filter.
1774 $enrolmethodfilter = new integer_filter('enrolments');
1775 $filterset->add_filter($enrolmethodfilter);
1777 // Configure the filter.
1778 foreach ($enrolmethods as $enrolmethod) {
1779 $enrolmethodfilter->add_filter_value($enrolinstancesmap[$enrolmethod]);
1781 $enrolmethodfilter->set_join_type($jointype);
1783 // Run the search.
1784 $search = new participants_search($course, $coursecontext, $filterset);
1785 $rs = $search->get_participants();
1786 $this->assertInstanceOf(moodle_recordset::class, $rs);
1787 $records = $this->convert_recordset_to_array($rs);
1788 $resetrecords = reset($records);
1789 $totalparticipants = $resetrecords->fullcount ?? 0;
1791 $this->assertCount($count, $records);
1792 $this->assertEquals($count, $totalparticipants);
1794 foreach ($expectedusers as $expecteduser) {
1795 $this->assertArrayHasKey($users[$expecteduser]->id, $records);
1800 * Data provider for enrolments filter tests.
1802 * @return array
1804 public function enrolments_provider(): array {
1805 $tests = [
1806 // Users with different enrolment methods.
1807 'Users with different enrolment methods' => (object) [
1808 'users' => [
1809 'a' => [
1810 'enrolmethods' => [
1811 'manual',
1814 'b' => [
1815 'enrolmethods' => [
1816 'self',
1819 'c' => [
1820 'enrolmethods' => [
1821 'manual',
1822 'self',
1826 'expect' => [
1827 // Tests for jointype: ANY.
1828 'ANY: No filter' => (object) [
1829 'enrolmethods' => [],
1830 'jointype' => filter::JOINTYPE_ANY,
1831 'count' => 3,
1832 'expectedusers' => [
1833 'a',
1834 'b',
1835 'c',
1838 'ANY: Filter by manual enrolments only' => (object) [
1839 'enrolmethods' => ['manual'],
1840 'jointype' => filter::JOINTYPE_ANY,
1841 'count' => 2,
1842 'expectedusers' => [
1843 'a',
1844 'c',
1847 'ANY: Filter by self enrolments only' => (object) [
1848 'enrolmethods' => ['self'],
1849 'jointype' => filter::JOINTYPE_ANY,
1850 'count' => 2,
1851 'expectedusers' => [
1852 'b',
1853 'c',
1856 'ANY: Filter by multiple enrolment methods' => (object) [
1857 'enrolmethods' => ['manual', 'self'],
1858 'jointype' => filter::JOINTYPE_ANY,
1859 'count' => 3,
1860 'expectedusers' => [
1861 'a',
1862 'b',
1863 'c',
1867 // Tests for jointype: ALL.
1868 'ALL: No filter' => (object) [
1869 'enrolmethods' => [],
1870 'jointype' => filter::JOINTYPE_ALL,
1871 'count' => 3,
1872 'expectedusers' => [
1873 'a',
1874 'b',
1875 'c',
1878 'ALL: Filter by manual enrolments only' => (object) [
1879 'enrolmethods' => ['manual'],
1880 'jointype' => filter::JOINTYPE_ALL,
1881 'count' => 2,
1882 'expectedusers' => [
1883 'a',
1884 'c',
1887 'ALL: Filter by multiple enrolment methods' => (object) [
1888 'enrolmethods' => ['manual', 'self'],
1889 'jointype' => filter::JOINTYPE_ALL,
1890 'count' => 1,
1891 'expectedusers' => [
1892 'c',
1896 // Tests for jointype: NONE.
1897 'NONE: No filter' => (object) [
1898 'enrolmethods' => [],
1899 'jointype' => filter::JOINTYPE_NONE,
1900 'count' => 3,
1901 'expectedusers' => [
1902 'a',
1903 'b',
1904 'c',
1907 'NONE: Filter by manual enrolments only' => (object) [
1908 'enrolmethods' => ['manual'],
1909 'jointype' => filter::JOINTYPE_NONE,
1910 'count' => 1,
1911 'expectedusers' => [
1912 'b',
1915 'NONE: Filter by multiple enrolment methods' => (object) [
1916 'enrolmethods' => ['manual', 'self'],
1917 'jointype' => filter::JOINTYPE_NONE,
1918 'count' => 0,
1919 'expectedusers' => [],
1925 $finaltests = [];
1926 foreach ($tests as $testname => $testdata) {
1927 foreach ($testdata->expect as $expectname => $expectdata) {
1928 $finaltests["{$testname} => {$expectname}"] = [
1929 'users' => $testdata->users,
1930 'enrolmethods' => $expectdata->enrolmethods,
1931 'jointype' => $expectdata->jointype,
1932 'count' => $expectdata->count,
1933 'expectedusers' => $expectdata->expectedusers,
1938 return $finaltests;
1942 * Ensure that the groups filter works as expected with the provided test cases.
1944 * @param array $usersdata The list of users to create
1945 * @param array $groupsavailable The names of groups that should be created in the course
1946 * @param array $filtergroups The names of groups to filter by
1947 * @param int $jointype The join type to use when combining filter values
1948 * @param int $count The expected count
1949 * @param array $expectedusers
1950 * @dataProvider groups_provider
1952 public function test_groups_filter(array $usersdata, array $groupsavailable, array $filtergroups, int $jointype, int $count,
1953 array $expectedusers): void {
1955 $course = $this->getDataGenerator()->create_course();
1956 $coursecontext = context_course::instance($course->id);
1957 $users = [];
1959 // Prepare data for filtering by users in no groups.
1960 $nogroupsdata = (object) [
1961 'id' => USERSWITHOUTGROUP,
1964 // Map group names to group data.
1965 $groupsdata = ['nogroups' => $nogroupsdata];
1966 foreach ($groupsavailable as $groupname) {
1967 $groupinfo = [
1968 'courseid' => $course->id,
1969 'name' => $groupname,
1972 $groupsdata[$groupname] = $this->getDataGenerator()->create_group($groupinfo);
1975 foreach ($usersdata as $username => $userdata) {
1976 $user = $this->getDataGenerator()->create_user(['username' => $username]);
1977 $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
1979 if (array_key_exists('groups', $userdata)) {
1980 foreach ($userdata['groups'] as $groupname) {
1981 $userinfo = [
1982 'userid' => $user->id,
1983 'groupid' => (int) $groupsdata[$groupname]->id,
1985 $this->getDataGenerator()->create_group_member($userinfo);
1989 $users[$username] = $user;
1992 // Create a secondary course with users. We should not see these users.
1993 $this->create_course_with_users(1, 1, 1, 1);
1995 // Create the basic filter.
1996 $filterset = new participants_filterset();
1997 $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id]));
1999 // Create the groups filter.
2000 $groupsfilter = new integer_filter('groups');
2001 $filterset->add_filter($groupsfilter);
2003 // Configure the filter.
2004 foreach ($filtergroups as $filtergroupname) {
2005 $groupsfilter->add_filter_value((int) $groupsdata[$filtergroupname]->id);
2007 $groupsfilter->set_join_type($jointype);
2009 // Run the search.
2010 $search = new participants_search($course, $coursecontext, $filterset);
2011 $rs = $search->get_participants();
2012 $this->assertInstanceOf(moodle_recordset::class, $rs);
2013 $records = $this->convert_recordset_to_array($rs);
2014 $resetrecords = reset($records);
2015 $totalparticipants = $resetrecords->fullcount ?? 0;
2017 $this->assertCount($count, $records);
2018 $this->assertEquals($count, $totalparticipants);
2020 foreach ($expectedusers as $expecteduser) {
2021 $this->assertArrayHasKey($users[$expecteduser]->id, $records);
2026 * Data provider for groups filter tests.
2028 * @return array
2030 public function groups_provider(): array {
2031 $tests = [
2032 'Users in different groups' => (object) [
2033 'groupsavailable' => [
2034 'groupa',
2035 'groupb',
2036 'groupc',
2038 'users' => [
2039 'a' => [
2040 'groups' => ['groupa'],
2042 'b' => [
2043 'groups' => ['groupb'],
2045 'c' => [
2046 'groups' => ['groupa', 'groupb'],
2048 'd' => [
2049 'groups' => [],
2052 'expect' => [
2053 // Tests for jointype: ANY.
2054 'ANY: No filter' => (object) [
2055 'groups' => [],
2056 'jointype' => filter::JOINTYPE_ANY,
2057 'count' => 4,
2058 'expectedusers' => [
2059 'a',
2060 'b',
2061 'c',
2062 'd',
2065 'ANY: Filter on a single group' => (object) [
2066 'groups' => ['groupa'],
2067 'jointype' => filter::JOINTYPE_ANY,
2068 'count' => 2,
2069 'expectedusers' => [
2070 'a',
2071 'c',
2074 'ANY: Filter on a group with no members' => (object) [
2075 'groups' => ['groupc'],
2076 'jointype' => filter::JOINTYPE_ANY,
2077 'count' => 0,
2078 'expectedusers' => [],
2080 'ANY: Filter on multiple groups' => (object) [
2081 'groups' => ['groupa', 'groupb'],
2082 'jointype' => filter::JOINTYPE_ANY,
2083 'count' => 3,
2084 'expectedusers' => [
2085 'a',
2086 'b',
2087 'c',
2090 'ANY: Filter on members of no groups only' => (object) [
2091 'groups' => ['nogroups'],
2092 'jointype' => filter::JOINTYPE_ANY,
2093 'count' => 1,
2094 'expectedusers' => [
2095 'd',
2098 'ANY: Filter on a single group or no groups' => (object) [
2099 'groups' => ['groupa', 'nogroups'],
2100 'jointype' => filter::JOINTYPE_ANY,
2101 'count' => 3,
2102 'expectedusers' => [
2103 'a',
2104 'c',
2105 'd',
2108 'ANY: Filter on multiple groups or no groups' => (object) [
2109 'groups' => ['groupa', 'groupb', 'nogroups'],
2110 'jointype' => filter::JOINTYPE_ANY,
2111 'count' => 4,
2112 'expectedusers' => [
2113 'a',
2114 'b',
2115 'c',
2116 'd',
2120 // Tests for jointype: ALL.
2121 'ALL: No filter' => (object) [
2122 'groups' => [],
2123 'jointype' => filter::JOINTYPE_ALL,
2124 'count' => 4,
2125 'expectedusers' => [
2126 'a',
2127 'b',
2128 'c',
2129 'd',
2132 'ALL: Filter on a single group' => (object) [
2133 'groups' => ['groupa'],
2134 'jointype' => filter::JOINTYPE_ALL,
2135 'count' => 2,
2136 'expectedusers' => [
2137 'a',
2138 'c',
2141 'ALL: Filter on a group with no members' => (object) [
2142 'groups' => ['groupc'],
2143 'jointype' => filter::JOINTYPE_ALL,
2144 'count' => 0,
2145 'expectedusers' => [],
2147 'ALL: Filter on members of no groups only' => (object) [
2148 'groups' => ['nogroups'],
2149 'jointype' => filter::JOINTYPE_ALL,
2150 'count' => 1,
2151 'expectedusers' => [
2152 'd',
2155 'ALL: Filter on multiple groups' => (object) [
2156 'groups' => ['groupa', 'groupb'],
2157 'jointype' => filter::JOINTYPE_ALL,
2158 'count' => 1,
2159 'expectedusers' => [
2160 'c',
2163 'ALL: Filter on a single group and no groups' => (object) [
2164 'groups' => ['groupa', 'nogroups'],
2165 'jointype' => filter::JOINTYPE_ALL,
2166 'count' => 0,
2167 'expectedusers' => [],
2169 'ALL: Filter on multiple groups and no groups' => (object) [
2170 'groups' => ['groupa', 'groupb', 'nogroups'],
2171 'jointype' => filter::JOINTYPE_ALL,
2172 'count' => 0,
2173 'expectedusers' => [],
2176 // Tests for jointype: NONE.
2177 'NONE: No filter' => (object) [
2178 'groups' => [],
2179 'jointype' => filter::JOINTYPE_NONE,
2180 'count' => 4,
2181 'expectedusers' => [
2182 'a',
2183 'b',
2184 'c',
2185 'd',
2188 'NONE: Filter on a single group' => (object) [
2189 'groups' => ['groupa'],
2190 'jointype' => filter::JOINTYPE_NONE,
2191 'count' => 2,
2192 'expectedusers' => [
2193 'b',
2194 'd',
2197 'NONE: Filter on a group with no members' => (object) [
2198 'groups' => ['groupc'],
2199 'jointype' => filter::JOINTYPE_NONE,
2200 'count' => 4,
2201 'expectedusers' => [
2202 'a',
2203 'b',
2204 'c',
2205 'd',
2208 'NONE: Filter on members of no groups only' => (object) [
2209 'groups' => ['nogroups'],
2210 'jointype' => filter::JOINTYPE_NONE,
2211 'count' => 3,
2212 'expectedusers' => [
2213 'a',
2214 'b',
2215 'c',
2218 'NONE: Filter on multiple groups' => (object) [
2219 'groups' => ['groupa', 'groupb'],
2220 'jointype' => filter::JOINTYPE_NONE,
2221 'count' => 1,
2222 'expectedusers' => [
2223 'd',
2226 'NONE: Filter on a single group and no groups' => (object) [
2227 'groups' => ['groupa', 'nogroups'],
2228 'jointype' => filter::JOINTYPE_NONE,
2229 'count' => 1,
2230 'expectedusers' => [
2231 'b',
2234 'NONE: Filter on multiple groups and no groups' => (object) [
2235 'groups' => ['groupa', 'groupb', 'nogroups'],
2236 'jointype' => filter::JOINTYPE_NONE,
2237 'count' => 0,
2238 'expectedusers' => [],
2244 $finaltests = [];
2245 foreach ($tests as $testname => $testdata) {
2246 foreach ($testdata->expect as $expectname => $expectdata) {
2247 $finaltests["{$testname} => {$expectname}"] = [
2248 'users' => $testdata->users,
2249 'groupsavailable' => $testdata->groupsavailable,
2250 'filtergroups' => $expectdata->groups,
2251 'jointype' => $expectdata->jointype,
2252 'count' => $expectdata->count,
2253 'expectedusers' => $expectdata->expectedusers,
2258 return $finaltests;
2262 * Ensure that the groups filter works as expected when separate groups mode is enabled, with the provided test cases.
2264 * @param array $usersdata The list of users to create
2265 * @param array $groupsavailable The names of groups that should be created in the course
2266 * @param array $filtergroups The names of groups to filter by
2267 * @param int $jointype The join type to use when combining filter values
2268 * @param int $count The expected count
2269 * @param array $expectedusers
2270 * @param string $loginusername The user to login as for the tests
2271 * @dataProvider groups_separate_provider
2273 public function test_groups_filter_separate_groups(array $usersdata, array $groupsavailable, array $filtergroups, int $jointype,
2274 int $count, array $expectedusers, string $loginusername): void {
2276 $course = $this->getDataGenerator()->create_course();
2277 $coursecontext = context_course::instance($course->id);
2278 $users = [];
2280 // Enable separate groups mode on the course.
2281 $course->groupmode = SEPARATEGROUPS;
2282 $course->groupmodeforce = true;
2283 update_course($course);
2285 // Prepare data for filtering by users in no groups.
2286 $nogroupsdata = (object) [
2287 'id' => USERSWITHOUTGROUP,
2290 // Map group names to group data.
2291 $groupsdata = ['nogroups' => $nogroupsdata];
2292 foreach ($groupsavailable as $groupname) {
2293 $groupinfo = [
2294 'courseid' => $course->id,
2295 'name' => $groupname,
2298 $groupsdata[$groupname] = $this->getDataGenerator()->create_group($groupinfo);
2301 foreach ($usersdata as $username => $userdata) {
2302 $user = $this->getDataGenerator()->create_user(['username' => $username]);
2303 $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
2305 if (array_key_exists('groups', $userdata)) {
2306 foreach ($userdata['groups'] as $groupname) {
2307 $userinfo = [
2308 'userid' => $user->id,
2309 'groupid' => (int) $groupsdata[$groupname]->id,
2311 $this->getDataGenerator()->create_group_member($userinfo);
2315 $users[$username] = $user;
2317 if ($username == $loginusername) {
2318 $loginuser = $user;
2322 // Create a secondary course with users. We should not see these users.
2323 $this->create_course_with_users(1, 1, 1, 1);
2325 // Log in as the user to be tested.
2326 $this->setUser($loginuser);
2328 // Create the basic filter.
2329 $filterset = new participants_filterset();
2330 $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id]));
2332 // Create the groups filter.
2333 $groupsfilter = new integer_filter('groups');
2334 $filterset->add_filter($groupsfilter);
2336 // Configure the filter.
2337 foreach ($filtergroups as $filtergroupname) {
2338 $groupsfilter->add_filter_value((int) $groupsdata[$filtergroupname]->id);
2340 $groupsfilter->set_join_type($jointype);
2342 // Run the search.
2343 $search = new participants_search($course, $coursecontext, $filterset);
2345 // Tests on user in no groups should throw an exception as they are not supported (participants are not visible to them).
2346 if (in_array('exception', $expectedusers)) {
2347 $this->expectException(\coding_exception::class);
2348 $rs = $search->get_participants();
2349 } else {
2350 // All other cases are tested as normal.
2351 $rs = $search->get_participants();
2352 $this->assertInstanceOf(moodle_recordset::class, $rs);
2353 $records = $this->convert_recordset_to_array($rs);
2354 $resetrecords = reset($records);
2355 $totalparticipants = $resetrecords->fullcount ?? 0;
2357 $this->assertCount($count, $records);
2358 $this->assertEquals($count, $totalparticipants);
2360 foreach ($expectedusers as $expecteduser) {
2361 $this->assertArrayHasKey($users[$expecteduser]->id, $records);
2367 * Data provider for groups filter tests.
2369 * @return array
2371 public function groups_separate_provider(): array {
2372 $tests = [
2373 'Users in different groups with separate groups mode enabled' => (object) [
2374 'groupsavailable' => [
2375 'groupa',
2376 'groupb',
2377 'groupc',
2379 'users' => [
2380 'a' => [
2381 'groups' => ['groupa'],
2383 'b' => [
2384 'groups' => ['groupb'],
2386 'c' => [
2387 'groups' => ['groupa', 'groupb'],
2389 'd' => [
2390 'groups' => [],
2393 'expect' => [
2394 // Tests for jointype: ANY.
2395 'ANY: No filter, user in one group' => (object) [
2396 'loginuser' => 'a',
2397 'groups' => [],
2398 'jointype' => filter::JOINTYPE_ANY,
2399 'count' => 2,
2400 'expectedusers' => [
2401 'a',
2402 'c',
2405 'ANY: No filter, user in multiple groups' => (object) [
2406 'loginuser' => 'c',
2407 'groups' => [],
2408 'jointype' => filter::JOINTYPE_ANY,
2409 'count' => 3,
2410 'expectedusers' => [
2411 'a',
2412 'b',
2413 'c',
2416 'ANY: No filter, user in no groups' => (object) [
2417 'loginuser' => 'd',
2418 'groups' => [],
2419 'jointype' => filter::JOINTYPE_ANY,
2420 'count' => 0,
2421 'expectedusers' => ['exception'],
2423 'ANY: Filter on a single group, user in one group' => (object) [
2424 'loginuser' => 'a',
2425 'groups' => ['groupa'],
2426 'jointype' => filter::JOINTYPE_ANY,
2427 'count' => 2,
2428 'expectedusers' => [
2429 'a',
2430 'c',
2433 'ANY: Filter on a single group, user in multple groups' => (object) [
2434 'loginuser' => 'c',
2435 'groups' => ['groupa'],
2436 'jointype' => filter::JOINTYPE_ANY,
2437 'count' => 2,
2438 'expectedusers' => [
2439 'a',
2440 'c',
2443 'ANY: Filter on a single group, user in no groups' => (object) [
2444 'loginuser' => 'd',
2445 'groups' => ['groupa'],
2446 'jointype' => filter::JOINTYPE_ANY,
2447 'count' => 0,
2448 'expectedusers' => ['exception'],
2450 'ANY: Filter on multiple groups, user in one group (ignore invalid groups)' => (object) [
2451 'loginuser' => 'a',
2452 'groups' => ['groupa', 'groupb'],
2453 'jointype' => filter::JOINTYPE_ANY,
2454 'count' => 2,
2455 'expectedusers' => [
2456 'a',
2457 'c',
2460 'ANY: Filter on multiple groups, user in multiple groups' => (object) [
2461 'loginuser' => 'c',
2462 'groups' => ['groupa', 'groupb'],
2463 'jointype' => filter::JOINTYPE_ANY,
2464 'count' => 3,
2465 'expectedusers' => [
2466 'a',
2467 'b',
2468 'c',
2471 'ANY: Filter on multiple groups or no groups, user in multiple groups (ignore no groups)' => (object) [
2472 'loginuser' => 'c',
2473 'groups' => ['groupa', 'groupb', 'nogroups'],
2474 'jointype' => filter::JOINTYPE_ANY,
2475 'count' => 3,
2476 'expectedusers' => [
2477 'a',
2478 'b',
2479 'c',
2483 // Tests for jointype: ALL.
2484 'ALL: No filter, user in one group' => (object) [
2485 'loginuser' => 'a',
2486 'groups' => [],
2487 'jointype' => filter::JOINTYPE_ALL,
2488 'count' => 2,
2489 'expectedusers' => [
2490 'a',
2491 'c',
2494 'ALL: No filter, user in multiple groups' => (object) [
2495 'loginuser' => 'c',
2496 'groups' => [],
2497 'jointype' => filter::JOINTYPE_ALL,
2498 'count' => 3,
2499 'expectedusers' => [
2500 'a',
2501 'b',
2502 'c',
2505 'ALL: No filter, user in no groups' => (object) [
2506 'loginuser' => 'd',
2507 'groups' => [],
2508 'jointype' => filter::JOINTYPE_ALL,
2509 'count' => 0,
2510 'expectedusers' => ['exception'],
2512 'ALL: Filter on a single group, user in one group' => (object) [
2513 'loginuser' => 'a',
2514 'groups' => ['groupa'],
2515 'jointype' => filter::JOINTYPE_ALL,
2516 'count' => 2,
2517 'expectedusers' => [
2518 'a',
2519 'c',
2522 'ALL: Filter on a single group, user in multple groups' => (object) [
2523 'loginuser' => 'c',
2524 'groups' => ['groupa'],
2525 'jointype' => filter::JOINTYPE_ALL,
2526 'count' => 2,
2527 'expectedusers' => [
2528 'a',
2529 'c',
2532 'ALL: Filter on a single group, user in no groups' => (object) [
2533 'loginuser' => 'd',
2534 'groups' => ['groupa'],
2535 'jointype' => filter::JOINTYPE_ALL,
2536 'count' => 0,
2537 'expectedusers' => ['exception'],
2539 'ALL: Filter on multiple groups, user in one group (ignore invalid groups)' => (object) [
2540 'loginuser' => 'a',
2541 'groups' => ['groupa', 'groupb'],
2542 'jointype' => filter::JOINTYPE_ALL,
2543 'count' => 2,
2544 'expectedusers' => [
2545 'a',
2546 'c',
2549 'ALL: Filter on multiple groups, user in multiple groups' => (object) [
2550 'loginuser' => 'c',
2551 'groups' => ['groupa', 'groupb'],
2552 'jointype' => filter::JOINTYPE_ALL,
2553 'count' => 1,
2554 'expectedusers' => [
2555 'c',
2558 'ALL: Filter on multiple groups or no groups, user in multiple groups (ignore no groups)' => (object) [
2559 'loginuser' => 'c',
2560 'groups' => ['groupa', 'groupb', 'nogroups'],
2561 'jointype' => filter::JOINTYPE_ALL,
2562 'count' => 1,
2563 'expectedusers' => [
2564 'c',
2568 // Tests for jointype: NONE.
2569 'NONE: No filter, user in one group' => (object) [
2570 'loginuser' => 'a',
2571 'groups' => [],
2572 'jointype' => filter::JOINTYPE_NONE,
2573 'count' => 2,
2574 'expectedusers' => [
2575 'a',
2576 'c',
2579 'NONE: No filter, user in multiple groups' => (object) [
2580 'loginuser' => 'c',
2581 'groups' => [],
2582 'jointype' => filter::JOINTYPE_NONE,
2583 'count' => 3,
2584 'expectedusers' => [
2585 'a',
2586 'b',
2587 'c',
2590 'NONE: No filter, user in no groups' => (object) [
2591 'loginuser' => 'd',
2592 'groups' => [],
2593 'jointype' => filter::JOINTYPE_NONE,
2594 'count' => 0,
2595 'expectedusers' => ['exception'],
2597 'NONE: Filter on a single group, user in one group' => (object) [
2598 'loginuser' => 'a',
2599 'groups' => ['groupa'],
2600 'jointype' => filter::JOINTYPE_NONE,
2601 'count' => 0,
2602 'expectedusers' => [],
2604 'NONE: Filter on a single group, user in multple groups' => (object) [
2605 'loginuser' => 'c',
2606 'groups' => ['groupa'],
2607 'jointype' => filter::JOINTYPE_NONE,
2608 'count' => 1,
2609 'expectedusers' => [
2610 'b',
2613 'NONE: Filter on a single group, user in no groups' => (object) [
2614 'loginuser' => 'd',
2615 'groups' => ['groupa'],
2616 'jointype' => filter::JOINTYPE_NONE,
2617 'count' => 0,
2618 'expectedusers' => ['exception'],
2620 'NONE: Filter on multiple groups, user in one group (ignore invalid groups)' => (object) [
2621 'loginuser' => 'a',
2622 'groups' => ['groupa', 'groupb'],
2623 'jointype' => filter::JOINTYPE_NONE,
2624 'count' => 0,
2625 'expectedusers' => [],
2627 'NONE: Filter on multiple groups, user in multiple groups' => (object) [
2628 'loginuser' => 'c',
2629 'groups' => ['groupa', 'groupb'],
2630 'jointype' => filter::JOINTYPE_NONE,
2631 'count' => 0,
2632 'expectedusers' => [],
2634 'NONE: Filter on multiple groups or no groups, user in multiple groups (ignore no groups)' => (object) [
2635 'loginuser' => 'c',
2636 'groups' => ['groupa', 'groupb', 'nogroups'],
2637 'jointype' => filter::JOINTYPE_NONE,
2638 'count' => 0,
2639 'expectedusers' => [],
2645 $finaltests = [];
2646 foreach ($tests as $testname => $testdata) {
2647 foreach ($testdata->expect as $expectname => $expectdata) {
2648 $finaltests["{$testname} => {$expectname}"] = [
2649 'users' => $testdata->users,
2650 'groupsavailable' => $testdata->groupsavailable,
2651 'filtergroups' => $expectdata->groups,
2652 'jointype' => $expectdata->jointype,
2653 'count' => $expectdata->count,
2654 'expectedusers' => $expectdata->expectedusers,
2655 'loginusername' => $expectdata->loginuser,
2660 return $finaltests;
2665 * Ensure that the last access filter works as expected with the provided test cases.
2667 * @param array $usersdata The list of users to create
2668 * @param array $accesssince The last access data to filter by
2669 * @param int $jointype The join type to use when combining filter values
2670 * @param int $count The expected count
2671 * @param array $expectedusers
2672 * @dataProvider accesssince_provider
2674 public function test_accesssince_filter(array $usersdata, array $accesssince, int $jointype, int $count,
2675 array $expectedusers): void {
2677 $course = $this->getDataGenerator()->create_course();
2678 $coursecontext = context_course::instance($course->id);
2679 $users = [];
2681 foreach ($usersdata as $username => $userdata) {
2682 $usertimestamp = empty($userdata['lastlogin']) ? 0 : strtotime($userdata['lastlogin']);
2684 $user = $this->getDataGenerator()->create_user(['username' => $username]);
2685 $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
2687 // Create the record of the user's last access to the course.
2688 if ($usertimestamp > 0) {
2689 $this->getDataGenerator()->create_user_course_lastaccess($user, $course, $usertimestamp);
2692 $users[$username] = $user;
2695 // Create a secondary course with users. We should not see these users.
2696 $this->create_course_with_users(1, 1, 1, 1);
2698 // Create the basic filter.
2699 $filterset = new participants_filterset();
2700 $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id]));
2702 // Create the last access filter.
2703 $lastaccessfilter = new integer_filter('accesssince');
2704 $filterset->add_filter($lastaccessfilter);
2706 // Configure the filter.
2707 foreach ($accesssince as $accessstring) {
2708 $lastaccessfilter->add_filter_value(strtotime($accessstring));
2710 $lastaccessfilter->set_join_type($jointype);
2712 // Run the search.
2713 $search = new participants_search($course, $coursecontext, $filterset);
2714 $rs = $search->get_participants();
2715 $this->assertInstanceOf(moodle_recordset::class, $rs);
2716 $records = $this->convert_recordset_to_array($rs);
2717 $resetrecords = reset($records);
2718 $totalparticipants = $resetrecords->fullcount ?? 0;
2720 $this->assertCount($count, $records);
2721 $this->assertEquals($count, $totalparticipants);
2723 foreach ($expectedusers as $expecteduser) {
2724 $this->assertArrayHasKey($users[$expecteduser]->id, $records);
2729 * Data provider for last access filter tests.
2731 * @return array
2733 public function accesssince_provider(): array {
2734 $tests = [
2735 // Users with different last access times.
2736 'Users in different groups' => (object) [
2737 'users' => [
2738 'a' => [
2739 'lastlogin' => '-3 days',
2741 'b' => [
2742 'lastlogin' => '-2 weeks',
2744 'c' => [
2745 'lastlogin' => '-5 months',
2747 'd' => [
2748 'lastlogin' => '-11 months',
2750 'e' => [
2751 // Never logged in.
2752 'lastlogin' => '',
2755 'expect' => [
2756 // Tests for jointype: ANY.
2757 'ANY: No filter' => (object) [
2758 'accesssince' => [],
2759 'jointype' => filter::JOINTYPE_ANY,
2760 'count' => 5,
2761 'expectedusers' => [
2762 'a',
2763 'b',
2764 'c',
2765 'd',
2766 'e',
2769 'ANY: Filter on last login more than 1 year ago' => (object) [
2770 'accesssince' => ['-1 year'],
2771 'jointype' => filter::JOINTYPE_ANY,
2772 'count' => 1,
2773 'expectedusers' => [
2774 'e',
2777 'ANY: Filter on last login more than 6 months ago' => (object) [
2778 'accesssince' => ['-6 months'],
2779 'jointype' => filter::JOINTYPE_ANY,
2780 'count' => 2,
2781 'expectedusers' => [
2782 'd',
2783 'e',
2786 'ANY: Filter on last login more than 3 weeks ago' => (object) [
2787 'accesssince' => ['-3 weeks'],
2788 'jointype' => filter::JOINTYPE_ANY,
2789 'count' => 3,
2790 'expectedusers' => [
2791 'c',
2792 'd',
2793 'e',
2796 'ANY: Filter on last login more than 5 days ago' => (object) [
2797 'accesssince' => ['-5 days'],
2798 'jointype' => filter::JOINTYPE_ANY,
2799 'count' => 4,
2800 'expectedusers' => [
2801 'b',
2802 'c',
2803 'd',
2804 'e',
2807 'ANY: Filter on last login more than 2 days ago' => (object) [
2808 'accesssince' => ['-2 days'],
2809 'jointype' => filter::JOINTYPE_ANY,
2810 'count' => 5,
2811 'expectedusers' => [
2812 'a',
2813 'b',
2814 'c',
2815 'd',
2816 'e',
2820 // Tests for jointype: ALL.
2821 'ALL: No filter' => (object) [
2822 'accesssince' => [],
2823 'jointype' => filter::JOINTYPE_ALL,
2824 'count' => 5,
2825 'expectedusers' => [
2826 'a',
2827 'b',
2828 'c',
2829 'd',
2830 'e',
2833 'ALL: Filter on last login more than 1 year ago' => (object) [
2834 'accesssince' => ['-1 year'],
2835 'jointype' => filter::JOINTYPE_ALL,
2836 'count' => 1,
2837 'expectedusers' => [
2838 'e',
2841 'ALL: Filter on last login more than 6 months ago' => (object) [
2842 'accesssince' => ['-6 months'],
2843 'jointype' => filter::JOINTYPE_ALL,
2844 'count' => 2,
2845 'expectedusers' => [
2846 'd',
2847 'e',
2850 'ALL: Filter on last login more than 3 weeks ago' => (object) [
2851 'accesssince' => ['-3 weeks'],
2852 'jointype' => filter::JOINTYPE_ALL,
2853 'count' => 3,
2854 'expectedusers' => [
2855 'c',
2856 'd',
2857 'e',
2860 'ALL: Filter on last login more than 5 days ago' => (object) [
2861 'accesssince' => ['-5 days'],
2862 'jointype' => filter::JOINTYPE_ALL,
2863 'count' => 4,
2864 'expectedusers' => [
2865 'b',
2866 'c',
2867 'd',
2868 'e',
2871 'ALL: Filter on last login more than 2 days ago' => (object) [
2872 'accesssince' => ['-2 days'],
2873 'jointype' => filter::JOINTYPE_ALL,
2874 'count' => 5,
2875 'expectedusers' => [
2876 'a',
2877 'b',
2878 'c',
2879 'd',
2880 'e',
2884 // Tests for jointype: NONE.
2885 'NONE: No filter' => (object) [
2886 'accesssince' => [],
2887 'jointype' => filter::JOINTYPE_NONE,
2888 'count' => 5,
2889 'expectedusers' => [
2890 'a',
2891 'b',
2892 'c',
2893 'd',
2894 'e',
2897 'NONE: Filter on last login more than 1 year ago' => (object) [
2898 'accesssince' => ['-1 year'],
2899 'jointype' => filter::JOINTYPE_NONE,
2900 'count' => 4,
2901 'expectedusers' => [
2902 'a',
2903 'b',
2904 'c',
2905 'd',
2908 'NONE: Filter on last login more than 6 months ago' => (object) [
2909 'accesssince' => ['-6 months'],
2910 'jointype' => filter::JOINTYPE_NONE,
2911 'count' => 3,
2912 'expectedusers' => [
2913 'a',
2914 'b',
2915 'c',
2918 'NONE: Filter on last login more than 3 weeks ago' => (object) [
2919 'accesssince' => ['-3 weeks'],
2920 'jointype' => filter::JOINTYPE_NONE,
2921 'count' => 2,
2922 'expectedusers' => [
2923 'a',
2924 'b',
2927 'NONE: Filter on last login more than 5 days ago' => (object) [
2928 'accesssince' => ['-5 days'],
2929 'jointype' => filter::JOINTYPE_NONE,
2930 'count' => 1,
2931 'expectedusers' => [
2932 'a',
2935 'NONE: Filter on last login more than 2 days ago' => (object) [
2936 'accesssince' => ['-2 days'],
2937 'jointype' => filter::JOINTYPE_NONE,
2938 'count' => 0,
2939 'expectedusers' => [],
2945 $finaltests = [];
2946 foreach ($tests as $testname => $testdata) {
2947 foreach ($testdata->expect as $expectname => $expectdata) {
2948 $finaltests["{$testname} => {$expectname}"] = [
2949 'users' => $testdata->users,
2950 'accesssince' => $expectdata->accesssince,
2951 'jointype' => $expectdata->jointype,
2952 'count' => $expectdata->count,
2953 'expectedusers' => $expectdata->expectedusers,
2958 return $finaltests;
2962 * Ensure that the joins between filters in the filterset work as expected with the provided test cases.
2964 * @param array $usersdata The list of users to create
2965 * @param array $filterdata The data to filter by
2966 * @param array $groupsavailable The names of groups that should be created in the course
2967 * @param int $jointype The join type to used between each filter being applied
2968 * @param int $count The expected count
2969 * @param array $expectedusers
2970 * @dataProvider filterset_joins_provider
2972 public function test_filterset_joins(array $usersdata, array $filterdata, array $groupsavailable, int $jointype, int $count,
2973 array $expectedusers): void {
2974 global $DB;
2976 // Ensure sufficient capabilities to view all statuses.
2977 $this->setAdminUser();
2979 // Remove the default role.
2980 set_config('roleid', 0, 'enrol_manual');
2982 $course = $this->getDataGenerator()->create_course();
2983 $coursecontext = context_course::instance($course->id);
2984 $roles = $DB->get_records_menu('role', [], '', 'shortname, id');
2985 $users = [];
2987 // Ensure all enrolment methods are enabled (and mapped where required for filtering later).
2988 $enrolinstances = enrol_get_instances($course->id, false);
2989 $enrolinstancesmap = [];
2990 foreach ($enrolinstances as $instance) {
2991 $plugin = enrol_get_plugin($instance->enrol);
2992 $plugin->update_status($instance, ENROL_INSTANCE_ENABLED);
2994 $enrolinstancesmap[$instance->enrol] = (int) $instance->id;
2997 // Create the required course groups and mapping.
2998 $nogroupsdata = (object) [
2999 'id' => USERSWITHOUTGROUP,
3002 $groupsdata = ['nogroups' => $nogroupsdata];
3003 foreach ($groupsavailable as $groupname) {
3004 $groupinfo = [
3005 'courseid' => $course->id,
3006 'name' => $groupname,
3009 $groupsdata[$groupname] = $this->getDataGenerator()->create_group($groupinfo);
3012 // Create test users.
3013 foreach ($usersdata as $username => $userdata) {
3014 $usertimestamp = empty($userdata['lastlogin']) ? 0 : strtotime($userdata['lastlogin']);
3015 unset($userdata['lastlogin']);
3017 // Prevent randomly generated field values that may cause false fails.
3018 $userdata['firstnamephonetic'] = $userdata['firstnamephonetic'] ?? $userdata['firstname'];
3019 $userdata['lastnamephonetic'] = $userdata['lastnamephonetic'] ?? $userdata['lastname'];
3020 $userdata['middlename'] = $userdata['middlename'] ?? '';
3021 $userdata['alternatename'] = $userdata['alternatename'] ?? $username;
3023 $user = $this->getDataGenerator()->create_user($userdata);
3025 foreach ($userdata['enrolments'] as $details) {
3026 $this->getDataGenerator()->enrol_user($user->id, $course->id, $roles[$details['role']],
3027 $details['method'], 0, 0, $details['status']);
3030 foreach ($userdata['groups'] as $groupname) {
3031 $userinfo = [
3032 'userid' => $user->id,
3033 'groupid' => (int) $groupsdata[$groupname]->id,
3035 $this->getDataGenerator()->create_group_member($userinfo);
3038 if ($usertimestamp > 0) {
3039 $this->getDataGenerator()->create_user_course_lastaccess($user, $course, $usertimestamp);
3042 $users[$username] = $user;
3045 // Create a secondary course with users. We should not see these users.
3046 $this->create_course_with_users(10, 10, 10, 10);
3048 // Create the basic filterset.
3049 $filterset = new participants_filterset();
3050 $filterset->set_join_type($jointype);
3051 $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id]));
3053 // Apply the keywords filter if required.
3054 if (array_key_exists('keywords', $filterdata)) {
3055 $keywordfilter = new string_filter('keywords');
3056 $filterset->add_filter($keywordfilter);
3058 foreach ($filterdata['keywords']['values'] as $keyword) {
3059 $keywordfilter->add_filter_value($keyword);
3061 $keywordfilter->set_join_type($filterdata['keywords']['jointype']);
3064 // Apply enrolment methods filter if required.
3065 if (array_key_exists('enrolmethods', $filterdata)) {
3066 $enrolmethodfilter = new integer_filter('enrolments');
3067 $filterset->add_filter($enrolmethodfilter);
3069 foreach ($filterdata['enrolmethods']['values'] as $enrolmethod) {
3070 $enrolmethodfilter->add_filter_value($enrolinstancesmap[$enrolmethod]);
3072 $enrolmethodfilter->set_join_type($filterdata['enrolmethods']['jointype']);
3075 // Apply roles filter if required.
3076 if (array_key_exists('courseroles', $filterdata)) {
3077 $rolefilter = new integer_filter('roles');
3078 $filterset->add_filter($rolefilter);
3080 foreach ($filterdata['courseroles']['values'] as $rolename) {
3081 $rolefilter->add_filter_value((int) $roles[$rolename]);
3083 $rolefilter->set_join_type($filterdata['courseroles']['jointype']);
3086 // Apply status filter if required.
3087 if (array_key_exists('status', $filterdata)) {
3088 $statusfilter = new integer_filter('status');
3089 $filterset->add_filter($statusfilter);
3091 foreach ($filterdata['status']['values'] as $status) {
3092 $statusfilter->add_filter_value($status);
3094 $statusfilter->set_join_type($filterdata['status']['jointype']);
3097 // Apply groups filter if required.
3098 if (array_key_exists('groups', $filterdata)) {
3099 $groupsfilter = new integer_filter('groups');
3100 $filterset->add_filter($groupsfilter);
3102 foreach ($filterdata['groups']['values'] as $filtergroupname) {
3103 $groupsfilter->add_filter_value((int) $groupsdata[$filtergroupname]->id);
3105 $groupsfilter->set_join_type($filterdata['groups']['jointype']);
3108 // Apply last access filter if required.
3109 if (array_key_exists('accesssince', $filterdata)) {
3110 $lastaccessfilter = new integer_filter('accesssince');
3111 $filterset->add_filter($lastaccessfilter);
3113 foreach ($filterdata['accesssince']['values'] as $accessstring) {
3114 $lastaccessfilter->add_filter_value(strtotime($accessstring));
3116 $lastaccessfilter->set_join_type($filterdata['accesssince']['jointype']);
3119 // Run the search.
3120 $search = new participants_search($course, $coursecontext, $filterset);
3121 $rs = $search->get_participants();
3122 $this->assertInstanceOf(moodle_recordset::class, $rs);
3123 $records = $this->convert_recordset_to_array($rs);
3124 $resetrecords = reset($records);
3125 $totalparticipants = $resetrecords->fullcount ?? 0;
3127 $this->assertCount($count, $records);
3128 $this->assertEquals($count, $totalparticipants);
3130 foreach ($expectedusers as $expecteduser) {
3131 $this->assertArrayHasKey($users[$expecteduser]->id, $records);
3136 * Data provider for filterset join tests.
3138 * @return array
3140 public function filterset_joins_provider(): array {
3141 $tests = [
3142 // Users with different configurations.
3143 'Users with different configurations' => (object) [
3144 'groupsavailable' => [
3145 'groupa',
3146 'groupb',
3147 'groupc',
3149 'users' => [
3150 'adam.ant' => [
3151 'firstname' => 'Adam',
3152 'lastname' => 'Ant',
3153 'enrolments' => [
3155 'role' => 'student',
3156 'method' => 'manual',
3157 'status' => ENROL_USER_ACTIVE,
3160 'groups' => ['groupa'],
3161 'lastlogin' => '-3 days',
3163 'barbara.bennett' => [
3164 'firstname' => 'Barbara',
3165 'lastname' => 'Bennett',
3166 'enrolments' => [
3168 'role' => 'student',
3169 'method' => 'manual',
3170 'status' => ENROL_USER_ACTIVE,
3173 'role' => 'teacher',
3174 'method' => 'manual',
3175 'status' => ENROL_USER_ACTIVE,
3178 'groups' => ['groupb'],
3179 'lastlogin' => '-2 weeks',
3181 'colin.carnforth' => [
3182 'firstname' => 'Colin',
3183 'lastname' => 'Carnforth',
3184 'enrolments' => [
3186 'role' => 'editingteacher',
3187 'method' => 'self',
3188 'status' => ENROL_USER_SUSPENDED,
3191 'groups' => ['groupa', 'groupb'],
3192 'lastlogin' => '-5 months',
3194 'tony.rogers' => [
3195 'firstname' => 'Anthony',
3196 'lastname' => 'Rogers',
3197 'enrolments' => [
3199 'role' => 'editingteacher',
3200 'method' => 'self',
3201 'status' => ENROL_USER_SUSPENDED,
3204 'groups' => [],
3205 'lastlogin' => '-10 months',
3207 'sarah.rester' => [
3208 'firstname' => 'Sarah',
3209 'lastname' => 'Rester',
3210 'email' => 'zazu@example.com',
3211 'enrolments' => [
3213 'role' => 'teacher',
3214 'method' => 'manual',
3215 'status' => ENROL_USER_ACTIVE,
3218 'role' => 'editingteacher',
3219 'method' => 'self',
3220 'status' => ENROL_USER_SUSPENDED,
3223 'groups' => [],
3224 'lastlogin' => '-11 months',
3226 'morgan.crikeyson' => [
3227 'firstname' => 'Morgan',
3228 'lastname' => 'Crikeyson',
3229 'enrolments' => [
3231 'role' => 'teacher',
3232 'method' => 'manual',
3233 'status' => ENROL_USER_ACTIVE,
3236 'groups' => ['groupa'],
3237 'lastlogin' => '-1 week',
3239 'jonathan.bravo' => [
3240 'firstname' => 'Jonathan',
3241 'lastname' => 'Bravo',
3242 'enrolments' => [
3244 'role' => 'student',
3245 'method' => 'manual',
3246 'status' => ENROL_USER_ACTIVE,
3249 'groups' => [],
3250 // Never logged in.
3251 'lastlogin' => '',
3254 'expect' => [
3255 // Tests for jointype: ANY.
3256 'ANY: No filters in filterset' => (object) [
3257 'filterdata' => [],
3258 'jointype' => filter::JOINTYPE_ANY,
3259 'count' => 7,
3260 'expectedusers' => [
3261 'adam.ant',
3262 'barbara.bennett',
3263 'colin.carnforth',
3264 'tony.rogers',
3265 'sarah.rester',
3266 'morgan.crikeyson',
3267 'jonathan.bravo',
3270 'ANY: Filterset containing a single filter type' => (object) [
3271 'filterdata' => [
3272 'enrolmethods' => [
3273 'values' => ['self'],
3274 'jointype' => filter::JOINTYPE_ANY,
3277 'jointype' => filter::JOINTYPE_ANY,
3278 'count' => 3,
3279 'expectedusers' => [
3280 'colin.carnforth',
3281 'tony.rogers',
3282 'sarah.rester',
3285 'ANY: Filterset matching all filter types on different users' => (object) [
3286 'filterdata' => [
3287 // Match Adam only.
3288 'keywords' => [
3289 'values' => ['adam'],
3290 'jointype' => filter::JOINTYPE_ALL,
3292 // Match Sarah only.
3293 'enrolmethods' => [
3294 'values' => ['manual', 'self'],
3295 'jointype' => filter::JOINTYPE_ALL,
3297 // Match Barbara only.
3298 'courseroles' => [
3299 'values' => ['student', 'teacher'],
3300 'jointype' => filter::JOINTYPE_ALL,
3302 // Match Sarah only.
3303 'status' => [
3304 'values' => [ENROL_USER_ACTIVE, ENROL_USER_SUSPENDED],
3305 'jointype' => filter::JOINTYPE_ALL,
3307 // Match Colin only.
3308 'groups' => [
3309 'values' => ['groupa', 'groupb'],
3310 'jointype' => filter::JOINTYPE_ALL,
3312 // Match Jonathan only.
3313 'accesssince' => [
3314 'values' => ['-1 year'],
3315 'jointype' => filter::JOINTYPE_ALL,
3318 'jointype' => filter::JOINTYPE_ANY,
3319 'count' => 5,
3320 // Morgan and Tony are not matched, to confirm filtering is not just returning all users.
3321 'expectedusers' => [
3322 'adam.ant',
3323 'barbara.bennett',
3324 'colin.carnforth',
3325 'sarah.rester',
3326 'jonathan.bravo',
3330 // Tests for jointype: ALL.
3331 'ALL: No filters in filterset' => (object) [
3332 'filterdata' => [],
3333 'jointype' => filter::JOINTYPE_ALL,
3334 'count' => 7,
3335 'expectedusers' => [
3336 'adam.ant',
3337 'barbara.bennett',
3338 'colin.carnforth',
3339 'tony.rogers',
3340 'sarah.rester',
3341 'morgan.crikeyson',
3342 'jonathan.bravo',
3345 'ALL: Filterset containing a single filter type' => (object) [
3346 'filterdata' => [
3347 'enrolmethods' => [
3348 'values' => ['self'],
3349 'jointype' => filter::JOINTYPE_ANY,
3352 'jointype' => filter::JOINTYPE_ALL,
3353 'count' => 3,
3354 'expectedusers' => [
3355 'colin.carnforth',
3356 'tony.rogers',
3357 'sarah.rester',
3360 'ALL: Filterset combining all filter types' => (object) [
3361 'filterdata' => [
3362 // Exclude Adam, Tony, Morgan and Jonathan.
3363 'keywords' => [
3364 'values' => ['ar'],
3365 'jointype' => filter::JOINTYPE_ANY,
3367 // Exclude Colin and Tony.
3368 'enrolmethods' => [
3369 'values' => ['manual'],
3370 'jointype' => filter::JOINTYPE_ANY,
3372 // Exclude Adam, Barbara and Jonathan.
3373 'courseroles' => [
3374 'values' => ['student'],
3375 'jointype' => filter::JOINTYPE_NONE,
3377 // Exclude Colin and Tony.
3378 'status' => [
3379 'values' => [ENROL_USER_ACTIVE],
3380 'jointype' => filter::JOINTYPE_ALL,
3382 // Exclude Barbara.
3383 'groups' => [
3384 'values' => ['groupa', 'nogroups'],
3385 'jointype' => filter::JOINTYPE_ANY,
3387 // Exclude Adam, Colin and Barbara.
3388 'accesssince' => [
3389 'values' => ['-6 months'],
3390 'jointype' => filter::JOINTYPE_ALL,
3393 'jointype' => filter::JOINTYPE_ALL,
3394 'count' => 1,
3395 'expectedusers' => [
3396 'sarah.rester',
3400 // Tests for jointype: NONE.
3401 'NONE: No filters in filterset' => (object) [
3402 'filterdata' => [],
3403 'jointype' => filter::JOINTYPE_NONE,
3404 'count' => 7,
3405 'expectedusers' => [
3406 'adam.ant',
3407 'barbara.bennett',
3408 'colin.carnforth',
3409 'tony.rogers',
3410 'sarah.rester',
3411 'morgan.crikeyson',
3412 'jonathan.bravo',
3415 'NONE: Filterset containing a single filter type' => (object) [
3416 'filterdata' => [
3417 'enrolmethods' => [
3418 'values' => ['self'],
3419 'jointype' => filter::JOINTYPE_ANY,
3422 'jointype' => filter::JOINTYPE_NONE,
3423 'count' => 4,
3424 'expectedusers' => [
3425 'adam.ant',
3426 'barbara.bennett',
3427 'morgan.crikeyson',
3428 'jonathan.bravo',
3431 'NONE: Filterset combining all filter types' => (object) [
3432 'filterdata' => [
3433 // Excludes Adam.
3434 'keywords' => [
3435 'values' => ['adam'],
3436 'jointype' => filter::JOINTYPE_ANY,
3438 // Excludes Colin, Tony and Sarah.
3439 'enrolmethods' => [
3440 'values' => ['self'],
3441 'jointype' => filter::JOINTYPE_ANY,
3443 // Excludes Jonathan.
3444 'courseroles' => [
3445 'values' => ['student'],
3446 'jointype' => filter::JOINTYPE_NONE,
3448 // Excludes Colin, Tony and Sarah.
3449 'status' => [
3450 'values' => [ENROL_USER_SUSPENDED],
3451 'jointype' => filter::JOINTYPE_ALL,
3453 // Excludes Adam, Colin, Tony, Sarah, Morgan and Jonathan.
3454 'groups' => [
3455 'values' => ['groupa', 'nogroups'],
3456 'jointype' => filter::JOINTYPE_ANY,
3458 // Excludes Tony and Sarah.
3459 'accesssince' => [
3460 'values' => ['-6 months'],
3461 'jointype' => filter::JOINTYPE_ALL,
3464 'jointype' => filter::JOINTYPE_NONE,
3465 'count' => 1,
3466 'expectedusers' => [
3467 'barbara.bennett',
3470 'NONE: Filterset combining several filter types and a double-negative on keyword' => (object) [
3471 'jointype' => filter::JOINTYPE_NONE,
3472 'filterdata' => [
3473 // Note: This is a jointype NONE on the parent jointype NONE.
3474 // The result therefore negated in this instance.
3475 // Include Adam and Anthony.
3476 'keywords' => [
3477 'values' => ['ant'],
3478 'jointype' => filter::JOINTYPE_NONE,
3480 // Excludes Tony.
3481 'status' => [
3482 'values' => [ENROL_USER_SUSPENDED],
3483 'jointype' => filter::JOINTYPE_ALL,
3486 'count' => 1,
3487 'expectedusers' => [
3488 'adam.ant',
3495 $finaltests = [];
3496 foreach ($tests as $testname => $testdata) {
3497 foreach ($testdata->expect as $expectname => $expectdata) {
3498 $finaltests["{$testname} => {$expectname}"] = [
3499 'users' => $testdata->users,
3500 'filterdata' => $expectdata->filterdata,
3501 'groupsavailable' => $testdata->groupsavailable,
3502 'jointype' => $expectdata->jointype,
3503 'count' => $expectdata->count,
3504 'expectedusers' => $expectdata->expectedusers,
3509 return $finaltests;