MDL-68348 user: Added filterset joins unit testing
[moodle.git] / user / tests / table / participants_search_test.php
blob3b6fc756294cf303285b5afb954f5aa101a1a2f2
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 /**
18 * Provides {@link core_user_table_participants_search_test} class.
20 * @package core_user
21 * @category test
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;
31 use context_course;
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;
38 use moodle_recordset;
39 use stdClass;
41 /**
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 {
49 /**
50 * Helper to convert a moodle_recordset to an array of records.
52 * @param moodle_recordset $recordset
53 * @return array
55 protected function convert_recordset_to_array(moodle_recordset $recordset): array {
56 $records = [];
57 foreach ($recordset as $record) {
58 $records[$record->id] = $record;
60 $recordset->close();
62 return $records;
65 /**
66 * Create and enrol a set of users into the specified course.
68 * @param stdClass $course
69 * @param int $count
70 * @param null|string $role
71 * @return array
73 protected function create_and_enrol_users(stdClass $course, int $count, ?string $role = null): array {
74 $this->resetAfterTest(true);
75 $users = [];
77 for ($i = 0; $i < $count; $i++) {
78 $user = $this->getDataGenerator()->create_user();
79 $this->getDataGenerator()->enrol_user($user->id, $course->id, $role);
80 $users[] = $user;
83 return $users;
86 /**
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.
93 * @return stdClass
95 protected function create_course_with_users(int $editingteachers, int $teachers, int $students, int $norole): stdClass {
96 $data = (object) [
97 'course' => $this->getDataGenerator()->create_course(),
98 'editingteachers' => [],
99 'teachers' => [],
100 'students' => [],
101 'norole' => [],
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);
111 return $data;
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 {
124 global $DB;
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);
137 $users = [];
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);
174 // Run the search.
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.
191 * @return array
193 public function role_provider(): array {
194 $tests = [
195 // Users who only have one role each.
196 'Users in each role' => (object) [
197 'users' => [
198 'a' => [
199 'courseroles' => [
200 'student',
203 'b' => [
204 'courseroles' => [
205 'student',
208 'c' => [
209 'courseroles' => [
210 'editingteacher',
213 'd' => [
214 'courseroles' => [
215 'editingteacher',
218 'e' => [
219 'courseroles' => [
220 'teacher',
223 'f' => [
224 'courseroles' => [
225 'teacher',
228 // User is enrolled in the course without role.
229 'g' => [
230 'courseroles' => [
234 // User is a category manager and also enrolled without role in the course.
235 'h' => [
236 'courseroles' => [
238 'categoryroles' => [
239 'manager',
243 // User is a category manager and not enrolled in the course.
244 // This user should not show up in any filter.
245 'i' => [
246 'categoryroles' => [
247 'manager',
251 'expect' => [
252 // Tests for jointype: ANY.
253 'ANY: No role filter' => (object) [
254 'roles' => [],
255 'jointype' => filter::JOINTYPE_ANY,
256 'count' => 8,
257 'expectedusers' => [
258 'a',
259 'b',
260 'c',
261 'd',
262 'e',
263 'f',
264 'g',
265 'h',
268 'ANY: Filter on student' => (object) [
269 'roles' => ['student'],
270 'jointype' => filter::JOINTYPE_ANY,
271 'count' => 2,
272 'expectedusers' => [
273 'a',
274 'b',
277 'ANY: Filter on student, teacher' => (object) [
278 'roles' => ['student', 'teacher'],
279 'jointype' => filter::JOINTYPE_ANY,
280 'count' => 4,
281 'expectedusers' => [
282 'a',
283 'b',
284 'e',
285 'f',
288 'ANY: Filter on student, manager (category level role)' => (object) [
289 'roles' => ['student', 'manager'],
290 'jointype' => filter::JOINTYPE_ANY,
291 'count' => 3,
292 'expectedusers' => [
293 'a',
294 'b',
295 'h',
298 'ANY: Filter on student, coursecreator (not assigned)' => (object) [
299 'roles' => ['student', 'coursecreator'],
300 'jointype' => filter::JOINTYPE_ANY,
301 'count' => 2,
302 'expectedusers' => [
303 'a',
304 'b',
308 // Tests for jointype: ALL.
309 'ALL: No role filter' => (object) [
310 'roles' => [],
311 'jointype' => filter::JOINTYPE_ALL,
312 'count' => 8,
313 'expectedusers' => [
314 'a',
315 'b',
316 'c',
317 'd',
318 'e',
319 'f',
320 'g',
321 'h',
324 'ALL: Filter on student' => (object) [
325 'roles' => ['student'],
326 'jointype' => filter::JOINTYPE_ALL,
327 'count' => 2,
328 'expectedusers' => [
329 'a',
330 'b',
333 'ALL: Filter on student, teacher' => (object) [
334 'roles' => ['student', 'teacher'],
335 'jointype' => filter::JOINTYPE_ALL,
336 'count' => 0,
337 'expectedusers' => [],
339 'ALL: Filter on student, manager (category level role))' => (object) [
340 'roles' => ['student', 'manager'],
341 'jointype' => filter::JOINTYPE_ALL,
342 'count' => 0,
343 'expectedusers' => [],
345 'ALL: Filter on student, coursecreator (not assigned))' => (object) [
346 'roles' => ['student', 'coursecreator'],
347 'jointype' => filter::JOINTYPE_ALL,
348 'count' => 0,
349 'expectedusers' => [],
352 // Tests for jointype: NONE.
353 'NONE: No role filter' => (object) [
354 'roles' => [],
355 'jointype' => filter::JOINTYPE_NONE,
356 'count' => 8,
357 'expectedusers' => [
358 'a',
359 'b',
360 'c',
361 'd',
362 'e',
363 'f',
364 'g',
365 'h',
368 'NONE: Filter on student' => (object) [
369 'roles' => ['student'],
370 'jointype' => filter::JOINTYPE_NONE,
371 'count' => 6,
372 'expectedusers' => [
373 'c',
374 'd',
375 'e',
376 'f',
377 'g',
378 'h',
381 'NONE: Filter on student, teacher' => (object) [
382 'roles' => ['student', 'teacher'],
383 'jointype' => filter::JOINTYPE_NONE,
384 'count' => 4,
385 'expectedusers' => [
386 'c',
387 'd',
388 'g',
389 'h',
392 'NONE: Filter on student, manager (category level role))' => (object) [
393 'roles' => ['student', 'manager'],
394 'jointype' => filter::JOINTYPE_NONE,
395 'count' => 5,
396 'expectedusers' => [
397 'c',
398 'd',
399 'e',
400 'f',
401 'g',
404 'NONE: Filter on student, coursecreator (not assigned))' => (object) [
405 'roles' => ['student', 'coursecreator'],
406 'jointype' => filter::JOINTYPE_NONE,
407 'count' => 6,
408 'expectedusers' => [
409 'c',
410 'd',
411 'e',
412 'f',
413 'g',
414 'h',
419 'Users with multiple roles' => (object) [
420 'users' => [
421 'a' => [
422 'courseroles' => [
423 'student',
426 'b' => [
427 'courseroles' => [
428 'student',
429 'teacher',
432 'c' => [
433 'courseroles' => [
434 'editingteacher',
437 'd' => [
438 'courseroles' => [
439 'editingteacher',
442 'e' => [
443 'courseroles' => [
444 'teacher',
445 'editingteacher',
448 'f' => [
449 'courseroles' => [
450 'teacher',
454 // User is enrolled in the course without role.
455 'g' => [
456 'courseroles' => [
460 // User is a category manager and also enrolled without role in the course.
461 'h' => [
462 'courseroles' => [
464 'categoryroles' => [
465 'manager',
469 // User is a category manager and not enrolled in the course.
470 // This user should not show up in any filter.
471 'i' => [
472 'categoryroles' => [
473 'manager',
477 'expect' => [
478 // Tests for jointype: ANY.
479 'ANY: No role filter' => (object) [
480 'roles' => [],
481 'jointype' => filter::JOINTYPE_ANY,
482 'count' => 8,
483 'expectedusers' => [
484 'a',
485 'b',
486 'c',
487 'd',
488 'e',
489 'f',
490 'g',
491 'h',
494 'ANY: Filter on student' => (object) [
495 'roles' => ['student'],
496 'jointype' => filter::JOINTYPE_ANY,
497 'count' => 2,
498 'expectedusers' => [
499 'a',
500 'b',
503 'ANY: Filter on teacher' => (object) [
504 'roles' => ['teacher'],
505 'jointype' => filter::JOINTYPE_ANY,
506 'count' => 3,
507 'expectedusers' => [
508 'b',
509 'e',
510 'f',
513 'ANY: Filter on editingteacher' => (object) [
514 'roles' => ['editingteacher'],
515 'jointype' => filter::JOINTYPE_ANY,
516 'count' => 3,
517 'expectedusers' => [
518 'c',
519 'd',
520 'e',
523 'ANY: Filter on student, teacher' => (object) [
524 'roles' => ['student', 'teacher'],
525 'jointype' => filter::JOINTYPE_ANY,
526 'count' => 4,
527 'expectedusers' => [
528 'a',
529 'b',
530 'e',
531 'f',
534 'ANY: Filter on teacher, editingteacher' => (object) [
535 'roles' => ['teacher', 'editingteacher'],
536 'jointype' => filter::JOINTYPE_ANY,
537 'count' => 5,
538 'expectedusers' => [
539 'b',
540 'c',
541 'd',
542 'e',
543 'f',
546 'ANY: Filter on student, manager (category level role)' => (object) [
547 'roles' => ['student', 'manager'],
548 'jointype' => filter::JOINTYPE_ANY,
549 'count' => 3,
550 'expectedusers' => [
551 'a',
552 'b',
553 'h',
556 'ANY: Filter on student, coursecreator (not assigned)' => (object) [
557 'roles' => ['student', 'coursecreator'],
558 'jointype' => filter::JOINTYPE_ANY,
559 'count' => 2,
560 'expectedusers' => [
561 'a',
562 'b',
566 // Tests for jointype: ALL.
567 'ALL: No role filter' => (object) [
568 'roles' => [],
569 'jointype' => filter::JOINTYPE_ALL,
570 'count' => 8,
571 'expectedusers' => [
572 'a',
573 'b',
574 'c',
575 'd',
576 'e',
577 'f',
578 'g',
579 'h',
582 'ALL: Filter on student' => (object) [
583 'roles' => ['student'],
584 'jointype' => filter::JOINTYPE_ALL,
585 'count' => 2,
586 'expectedusers' => [
587 'a',
588 'b',
591 'ALL: Filter on teacher' => (object) [
592 'roles' => ['teacher'],
593 'jointype' => filter::JOINTYPE_ALL,
594 'count' => 3,
595 'expectedusers' => [
596 'b',
597 'e',
598 'f',
601 'ALL: Filter on editingteacher' => (object) [
602 'roles' => ['editingteacher'],
603 'jointype' => filter::JOINTYPE_ALL,
604 'count' => 3,
605 'expectedusers' => [
606 'c',
607 'd',
608 'e',
611 'ALL: Filter on student, teacher' => (object) [
612 'roles' => ['student', 'teacher'],
613 'jointype' => filter::JOINTYPE_ALL,
614 'count' => 1,
615 'expectedusers' => [
616 'b',
619 'ALL: Filter on teacher, editingteacher' => (object) [
620 'roles' => ['teacher', 'editingteacher'],
621 'jointype' => filter::JOINTYPE_ALL,
622 'count' => 1,
623 'expectedusers' => [
624 'e',
627 'ALL: Filter on student, manager (category level role)' => (object) [
628 'roles' => ['student', 'manager'],
629 'jointype' => filter::JOINTYPE_ALL,
630 'count' => 0,
631 'expectedusers' => [],
633 'ALL: Filter on student, coursecreator (not assigned)' => (object) [
634 'roles' => ['student', 'coursecreator'],
635 'jointype' => filter::JOINTYPE_ALL,
636 'count' => 0,
637 'expectedusers' => [],
640 // Tests for jointype: NONE.
641 'NONE: No role filter' => (object) [
642 'roles' => [],
643 'jointype' => filter::JOINTYPE_NONE,
644 'count' => 8,
645 'expectedusers' => [
646 'a',
647 'b',
648 'c',
649 'd',
650 'e',
651 'f',
652 'g',
653 'h',
656 'NONE: Filter on student' => (object) [
657 'roles' => ['student'],
658 'jointype' => filter::JOINTYPE_NONE,
659 'count' => 6,
660 'expectedusers' => [
661 'c',
662 'd',
663 'e',
664 'f',
665 'g',
666 'h',
669 'NONE: Filter on teacher' => (object) [
670 'roles' => ['teacher'],
671 'jointype' => filter::JOINTYPE_NONE,
672 'count' => 5,
673 'expectedusers' => [
674 'a',
675 'c',
676 'd',
677 'g',
678 'h',
681 'NONE: Filter on editingteacher' => (object) [
682 'roles' => ['editingteacher'],
683 'jointype' => filter::JOINTYPE_NONE,
684 'count' => 5,
685 'expectedusers' => [
686 'a',
687 'b',
688 'f',
689 'g',
690 'h',
693 'NONE: Filter on student, teacher' => (object) [
694 'roles' => ['student', 'teacher'],
695 'jointype' => filter::JOINTYPE_NONE,
696 'count' => 5,
697 'expectedusers' => [
698 'c',
699 'd',
700 'e',
701 'g',
702 'h',
705 'NONE: Filter on student, teacher' => (object) [
706 'roles' => ['teacher', 'editingteacher'],
707 'jointype' => filter::JOINTYPE_NONE,
708 'count' => 3,
709 'expectedusers' => [
710 'a',
711 'g',
712 'h',
715 'NONE: Filter on student, manager (category level role)' => (object) [
716 'roles' => ['student', 'manager'],
717 'jointype' => filter::JOINTYPE_NONE,
718 'count' => 5,
719 'expectedusers' => [
720 'c',
721 'd',
722 'e',
723 'f',
724 'g',
727 'NONE: Filter on student, coursecreator (not assigned)' => (object) [
728 'roles' => ['student', 'coursecreator'],
729 'jointype' => filter::JOINTYPE_NONE,
730 'count' => 6,
731 'expectedusers' => [
732 'c',
733 'd',
734 'e',
735 'f',
736 'g',
737 'h',
744 $finaltests = [];
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,
757 return $finaltests;
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);
773 $users = [];
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);
804 // Run the search.
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.
821 * @return array
823 public function keywords_provider(): array {
824 $tests = [
825 // Users where the keyword matches basic user fields such as names and email.
826 'Users with basic names' => (object) [
827 'users' => [
828 'adam.ant' => [
829 'firstname' => 'Adam',
830 'lastname' => 'Ant',
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',
844 'tony.rogers' => [
845 'firstname' => 'Anthony',
846 'lastname' => 'Rogers',
847 'lastnamephonetic' => 'Rowjours',
849 'sarah.rester' => [
850 'firstname' => 'Sarah',
851 'lastname' => 'Rester',
852 'email' => 'zazu@example.com',
853 'firstnamephonetic' => 'Sera',
856 'expect' => [
857 // Tests for jointype: ANY.
858 'ANY: No filter' => (object) [
859 'keywords' => [],
860 'jointype' => filter::JOINTYPE_ANY,
861 'count' => 5,
862 'expectedusers' => [
863 'adam.ant',
864 'barbara.bennett',
865 'colin.carnforth',
866 'tony.rogers',
867 'sarah.rester',
870 'ANY: Filter on first name only' => (object) [
871 'keywords' => ['adam'],
872 'jointype' => filter::JOINTYPE_ANY,
873 'count' => 1,
874 'expectedusers' => [
875 'adam.ant',
878 'ANY: Filter on last name only' => (object) [
879 'keywords' => ['BeNNeTt'],
880 'jointype' => filter::JOINTYPE_ANY,
881 'count' => 1,
882 'expectedusers' => [
883 'barbara.bennett',
886 'ANY: Filter on first/Last name' => (object) [
887 'keywords' => ['ant'],
888 'jointype' => filter::JOINTYPE_ANY,
889 'count' => 2,
890 'expectedusers' => [
891 'adam.ant',
892 'tony.rogers',
895 'ANY: Filter on middlename only' => (object) [
896 'keywords' => ['Jeff'],
897 'jointype' => filter::JOINTYPE_ANY,
898 'count' => 1,
899 'expectedusers' => [
900 'colin.carnforth',
903 'ANY: Filter on username (no match)' => (object) [
904 'keywords' => ['sara.rester'],
905 'jointype' => filter::JOINTYPE_ANY,
906 'count' => 0,
907 'expectedusers' => [],
909 'ANY: Filter on email only' => (object) [
910 'keywords' => ['zazu'],
911 'jointype' => filter::JOINTYPE_ANY,
912 'count' => 1,
913 'expectedusers' => [
914 'sarah.rester',
917 'ANY: Filter on first name phonetic only' => (object) [
918 'keywords' => ['Sera'],
919 'jointype' => filter::JOINTYPE_ANY,
920 'count' => 1,
921 'expectedusers' => [
922 'sarah.rester',
925 'ANY: Filter on last name phonetic only' => (object) [
926 'keywords' => ['jour'],
927 'jointype' => filter::JOINTYPE_ANY,
928 'count' => 1,
929 'expectedusers' => [
930 'tony.rogers',
933 'ANY: Filter on alternate name only' => (object) [
934 'keywords' => ['Babs'],
935 'jointype' => filter::JOINTYPE_ANY,
936 'count' => 1,
937 'expectedusers' => [
938 'barbara.bennett',
941 'ANY: Filter on multiple keywords (first/middle/last name)' => (object) [
942 'keywords' => ['ant', 'Jeff', 'rog'],
943 'jointype' => filter::JOINTYPE_ANY,
944 'count' => 3,
945 'expectedusers' => [
946 'adam.ant',
947 'colin.carnforth',
948 'tony.rogers',
951 'ANY: Filter on multiple keywords (phonetic/alternate names)' => (object) [
952 'keywords' => ['era', 'Bab', 'ours'],
953 'jointype' => filter::JOINTYPE_ANY,
954 'count' => 3,
955 'expectedusers' => [
956 'barbara.bennett',
957 'sarah.rester',
958 'tony.rogers',
962 // Tests for jointype: ALL.
963 'ALL: No filter' => (object) [
964 'keywords' => [],
965 'jointype' => filter::JOINTYPE_ALL,
966 'count' => 5,
967 'expectedusers' => [
968 'adam.ant',
969 'barbara.bennett',
970 'colin.carnforth',
971 'tony.rogers',
972 'sarah.rester',
975 'ALL: Filter on first name only' => (object) [
976 'keywords' => ['adam'],
977 'jointype' => filter::JOINTYPE_ALL,
978 'count' => 1,
979 'expectedusers' => [
980 'adam.ant',
983 'ALL: Filter on last name only' => (object) [
984 'keywords' => ['BeNNeTt'],
985 'jointype' => filter::JOINTYPE_ALL,
986 'count' => 1,
987 'expectedusers' => [
988 'barbara.bennett',
991 'ALL: Filter on first/Last name' => (object) [
992 'keywords' => ['ant'],
993 'jointype' => filter::JOINTYPE_ALL,
994 'count' => 2,
995 'expectedusers' => [
996 'adam.ant',
997 'tony.rogers',
1000 'ALL: Filter on middlename only' => (object) [
1001 'keywords' => ['Jeff'],
1002 'jointype' => filter::JOINTYPE_ALL,
1003 'count' => 1,
1004 'expectedusers' => [
1005 'colin.carnforth',
1008 'ALL: Filter on username (no match)' => (object) [
1009 'keywords' => ['sara.rester'],
1010 'jointype' => filter::JOINTYPE_ALL,
1011 'count' => 0,
1012 'expectedusers' => [],
1014 'ALL: Filter on email only' => (object) [
1015 'keywords' => ['zazu'],
1016 'jointype' => filter::JOINTYPE_ALL,
1017 'count' => 1,
1018 'expectedusers' => [
1019 'sarah.rester',
1022 'ALL: Filter on first name phonetic only' => (object) [
1023 'keywords' => ['Sera'],
1024 'jointype' => filter::JOINTYPE_ALL,
1025 'count' => 1,
1026 'expectedusers' => [
1027 'sarah.rester',
1030 'ALL: Filter on last name phonetic only' => (object) [
1031 'keywords' => ['jour'],
1032 'jointype' => filter::JOINTYPE_ALL,
1033 'count' => 1,
1034 'expectedusers' => [
1035 'tony.rogers',
1038 'ALL: Filter on alternate name only' => (object) [
1039 'keywords' => ['Babs'],
1040 'jointype' => filter::JOINTYPE_ALL,
1041 'count' => 1,
1042 'expectedusers' => [
1043 'barbara.bennett',
1046 'ALL: Filter on multiple keywords (first/last name)' => (object) [
1047 'keywords' => ['ant', 'rog'],
1048 'jointype' => filter::JOINTYPE_ALL,
1049 'count' => 1,
1050 'expectedusers' => [
1051 'tony.rogers',
1054 'ALL: Filter on multiple keywords (first/middle/last name)' => (object) [
1055 'keywords' => ['ant', 'Jeff', 'rog'],
1056 'jointype' => filter::JOINTYPE_ALL,
1057 'count' => 0,
1058 'expectedusers' => [],
1060 'ALL: Filter on multiple keywords (phonetic/alternate names)' => (object) [
1061 'keywords' => ['Bab', 'bra', 'nit'],
1062 'jointype' => filter::JOINTYPE_ALL,
1063 'count' => 1,
1064 'expectedusers' => [
1065 'barbara.bennett',
1069 // Tests for jointype: NONE.
1070 'NONE: No filter' => (object) [
1071 'keywords' => [],
1072 'jointype' => filter::JOINTYPE_NONE,
1073 'count' => 5,
1074 'expectedusers' => [
1075 'adam.ant',
1076 'barbara.bennett',
1077 'colin.carnforth',
1078 'tony.rogers',
1079 'sarah.rester',
1082 'NONE: Filter on first name only' => (object) [
1083 'keywords' => ['ara'],
1084 'jointype' => filter::JOINTYPE_NONE,
1085 'count' => 3,
1086 'expectedusers' => [
1087 'adam.ant',
1088 'colin.carnforth',
1089 'tony.rogers',
1092 'NONE: Filter on last name only' => (object) [
1093 'keywords' => ['BeNNeTt'],
1094 'jointype' => filter::JOINTYPE_NONE,
1095 'count' => 4,
1096 'expectedusers' => [
1097 'adam.ant',
1098 'colin.carnforth',
1099 'tony.rogers',
1100 'sarah.rester',
1103 'NONE: Filter on first/Last name' => (object) [
1104 'keywords' => ['ar'],
1105 'jointype' => filter::JOINTYPE_NONE,
1106 'count' => 2,
1107 'expectedusers' => [
1108 'adam.ant',
1109 'tony.rogers',
1112 'NONE: Filter on middlename only' => (object) [
1113 'keywords' => ['Jeff'],
1114 'jointype' => filter::JOINTYPE_NONE,
1115 'count' => 4,
1116 'expectedusers' => [
1117 'adam.ant',
1118 'barbara.bennett',
1119 'tony.rogers',
1120 'sarah.rester',
1123 'NONE: Filter on username (no match)' => (object) [
1124 'keywords' => ['sara.rester'],
1125 'jointype' => filter::JOINTYPE_NONE,
1126 'count' => 5,
1127 'expectedusers' => [
1128 'adam.ant',
1129 'barbara.bennett',
1130 'colin.carnforth',
1131 'tony.rogers',
1132 'sarah.rester',
1135 'NONE: Filter on email' => (object) [
1136 'keywords' => ['zazu'],
1137 'jointype' => filter::JOINTYPE_NONE,
1138 'count' => 4,
1139 'expectedusers' => [
1140 'adam.ant',
1141 'barbara.bennett',
1142 'colin.carnforth',
1143 'tony.rogers',
1146 'NONE: Filter on first name phonetic only' => (object) [
1147 'keywords' => ['Sera'],
1148 'jointype' => filter::JOINTYPE_NONE,
1149 'count' => 4,
1150 'expectedusers' => [
1151 'adam.ant',
1152 'barbara.bennett',
1153 'colin.carnforth',
1154 'tony.rogers',
1157 'NONE: Filter on last name phonetic only' => (object) [
1158 'keywords' => ['jour'],
1159 'jointype' => filter::JOINTYPE_NONE,
1160 'count' => 4,
1161 'expectedusers' => [
1162 'adam.ant',
1163 'barbara.bennett',
1164 'colin.carnforth',
1165 'sarah.rester',
1168 'NONE: Filter on alternate name only' => (object) [
1169 'keywords' => ['Babs'],
1170 'jointype' => filter::JOINTYPE_NONE,
1171 'count' => 4,
1172 'expectedusers' => [
1173 'adam.ant',
1174 'colin.carnforth',
1175 'tony.rogers',
1176 'sarah.rester',
1179 'NONE: Filter on multiple keywords (first/last name)' => (object) [
1180 'keywords' => ['ara', 'rog'],
1181 'jointype' => filter::JOINTYPE_NONE,
1182 'count' => 2,
1183 'expectedusers' => [
1184 'adam.ant',
1185 'colin.carnforth',
1188 'NONE: Filter on multiple keywords (first/middle/last name)' => (object) [
1189 'keywords' => ['ant', 'Jeff', 'rog'],
1190 'jointype' => filter::JOINTYPE_NONE,
1191 'count' => 2,
1192 'expectedusers' => [
1193 'barbara.bennett',
1194 'sarah.rester',
1197 'NONE: Filter on multiple keywords (phonetic/alternate names)' => (object) [
1198 'keywords' => ['Bab', 'bra', 'nit'],
1199 'jointype' => filter::JOINTYPE_NONE,
1200 'count' => 4,
1201 'expectedusers' => [
1202 'adam.ant',
1203 'colin.carnforth',
1204 'tony.rogers',
1205 'sarah.rester',
1212 $finaltests = [];
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,
1225 return $finaltests;
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);
1241 $users = [];
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);
1282 // Run the search.
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.
1299 * @return array
1301 public function status_provider(): array {
1302 $tests = [
1303 // Users with different statuses and enrolment methods (so multiple statuses are possible for the same user).
1304 'Users with different enrolment statuses' => (object) [
1305 'users' => [
1306 'a' => [
1307 'statuses' => [
1308 'manual' => ENROL_USER_ACTIVE,
1311 'b' => [
1312 'statuses' => [
1313 'self' => ENROL_USER_ACTIVE,
1316 'c' => [
1317 'statuses' => [
1318 'manual' => ENROL_USER_SUSPENDED,
1321 'd' => [
1322 'statuses' => [
1323 'self' => ENROL_USER_SUSPENDED,
1326 'e' => [
1327 'statuses' => [
1328 'manual' => ENROL_USER_ACTIVE,
1329 'self' => ENROL_USER_SUSPENDED,
1333 'expect' => [
1334 // Tests for jointype: ANY.
1335 'ANY: No filter' => (object) [
1336 'statuses' => [],
1337 'jointype' => filter::JOINTYPE_ANY,
1338 'count' => 5,
1339 'expectedusers' => [
1340 'a',
1341 'b',
1342 'c',
1343 'd',
1344 'e',
1347 'ANY: Filter on active only' => (object) [
1348 'statuses' => [ENROL_USER_ACTIVE],
1349 'jointype' => filter::JOINTYPE_ANY,
1350 'count' => 3,
1351 'expectedusers' => [
1352 'a',
1353 'b',
1354 'e',
1357 'ANY: Filter on suspended only' => (object) [
1358 'statuses' => [ENROL_USER_SUSPENDED],
1359 'jointype' => filter::JOINTYPE_ANY,
1360 'count' => 3,
1361 'expectedusers' => [
1362 'c',
1363 'd',
1364 'e',
1367 'ANY: Filter on multiple statuses' => (object) [
1368 'statuses' => [ENROL_USER_ACTIVE, ENROL_USER_SUSPENDED],
1369 'jointype' => filter::JOINTYPE_ANY,
1370 'count' => 5,
1371 'expectedusers' => [
1372 'a',
1373 'b',
1374 'c',
1375 'd',
1376 'e',
1380 // Tests for jointype: ALL.
1381 'ALL: No filter' => (object) [
1382 'statuses' => [],
1383 'jointype' => filter::JOINTYPE_ALL,
1384 'count' => 5,
1385 'expectedusers' => [
1386 'a',
1387 'b',
1388 'c',
1389 'd',
1390 'e',
1393 'ALL: Filter on active only' => (object) [
1394 'statuses' => [ENROL_USER_ACTIVE],
1395 'jointype' => filter::JOINTYPE_ALL,
1396 'count' => 3,
1397 'expectedusers' => [
1398 'a',
1399 'b',
1400 'e',
1403 'ALL: Filter on suspended only' => (object) [
1404 'statuses' => [ENROL_USER_SUSPENDED],
1405 'jointype' => filter::JOINTYPE_ALL,
1406 'count' => 3,
1407 'expectedusers' => [
1408 'c',
1409 'd',
1410 'e',
1413 'ALL: Filter on multiple statuses' => (object) [
1414 'statuses' => [ENROL_USER_ACTIVE, ENROL_USER_SUSPENDED],
1415 'jointype' => filter::JOINTYPE_ALL,
1416 'count' => 1,
1417 'expectedusers' => [
1418 'e',
1422 // Tests for jointype: NONE.
1423 'NONE: No filter' => (object) [
1424 'statuses' => [],
1425 'jointype' => filter::JOINTYPE_NONE,
1426 'count' => 5,
1427 'expectedusers' => [
1428 'a',
1429 'b',
1430 'c',
1431 'd',
1432 'e',
1435 'NONE: Filter on active only' => (object) [
1436 'statuses' => [ENROL_USER_ACTIVE],
1437 'jointype' => filter::JOINTYPE_NONE,
1438 'count' => 3,
1439 'expectedusers' => [
1440 'c',
1441 'd',
1442 'e',
1445 'NONE: Filter on suspended only' => (object) [
1446 'statuses' => [ENROL_USER_SUSPENDED],
1447 'jointype' => filter::JOINTYPE_NONE,
1448 'count' => 3,
1449 'expectedusers' => [
1450 'a',
1451 'b',
1452 'e',
1455 'NONE: Filter on multiple statuses' => (object) [
1456 'statuses' => [ENROL_USER_ACTIVE, ENROL_USER_SUSPENDED],
1457 'jointype' => filter::JOINTYPE_NONE,
1458 'count' => 0,
1459 'expectedusers' => [],
1465 $finaltests = [];
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,
1478 return $finaltests;
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);
1496 $users = [];
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);
1537 // Run the search.
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.
1554 * @return array
1556 public function enrolments_provider(): array {
1557 $tests = [
1558 // Users with different enrolment methods.
1559 'Users with different enrolment methods' => (object) [
1560 'users' => [
1561 'a' => [
1562 'enrolmethods' => [
1563 'manual',
1566 'b' => [
1567 'enrolmethods' => [
1568 'self',
1571 'c' => [
1572 'enrolmethods' => [
1573 'manual',
1574 'self',
1578 'expect' => [
1579 // Tests for jointype: ANY.
1580 'ANY: No filter' => (object) [
1581 'enrolmethods' => [],
1582 'jointype' => filter::JOINTYPE_ANY,
1583 'count' => 3,
1584 'expectedusers' => [
1585 'a',
1586 'b',
1587 'c',
1590 'ANY: Filter by manual enrolments only' => (object) [
1591 'enrolmethods' => ['manual'],
1592 'jointype' => filter::JOINTYPE_ANY,
1593 'count' => 2,
1594 'expectedusers' => [
1595 'a',
1596 'c',
1599 'ANY: Filter by self enrolments only' => (object) [
1600 'enrolmethods' => ['self'],
1601 'jointype' => filter::JOINTYPE_ANY,
1602 'count' => 2,
1603 'expectedusers' => [
1604 'b',
1605 'c',
1608 'ANY: Filter by multiple enrolment methods' => (object) [
1609 'enrolmethods' => ['manual', 'self'],
1610 'jointype' => filter::JOINTYPE_ANY,
1611 'count' => 3,
1612 'expectedusers' => [
1613 'a',
1614 'b',
1615 'c',
1619 // Tests for jointype: ALL.
1620 'ALL: No filter' => (object) [
1621 'enrolmethods' => [],
1622 'jointype' => filter::JOINTYPE_ALL,
1623 'count' => 3,
1624 'expectedusers' => [
1625 'a',
1626 'b',
1627 'c',
1630 'ALL: Filter by manual enrolments only' => (object) [
1631 'enrolmethods' => ['manual'],
1632 'jointype' => filter::JOINTYPE_ALL,
1633 'count' => 2,
1634 'expectedusers' => [
1635 'a',
1636 'c',
1639 'ALL: Filter by multiple enrolment methods' => (object) [
1640 'enrolmethods' => ['manual', 'self'],
1641 'jointype' => filter::JOINTYPE_ALL,
1642 'count' => 1,
1643 'expectedusers' => [
1644 'c',
1648 // Tests for jointype: NONE.
1649 'NONE: No filter' => (object) [
1650 'enrolmethods' => [],
1651 'jointype' => filter::JOINTYPE_NONE,
1652 'count' => 3,
1653 'expectedusers' => [
1654 'a',
1655 'b',
1656 'c',
1659 'NONE: Filter by manual enrolments only' => (object) [
1660 'enrolmethods' => ['manual'],
1661 'jointype' => filter::JOINTYPE_NONE,
1662 'count' => 1,
1663 'expectedusers' => [
1664 'b',
1667 'NONE: Filter by multiple enrolment methods' => (object) [
1668 'enrolmethods' => ['manual', 'self'],
1669 'jointype' => filter::JOINTYPE_NONE,
1670 'count' => 0,
1671 'expectedusers' => [],
1677 $finaltests = [];
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,
1690 return $finaltests;
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);
1709 $users = [];
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) {
1719 $groupinfo = [
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) {
1733 $userinfo = [
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);
1761 // Run the search.
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.
1778 * @return array
1780 public function groups_provider(): array {
1781 $tests = [
1782 'Users in different groups' => (object) [
1783 'groupsavailable' => [
1784 'groupa',
1785 'groupb',
1786 'groupc',
1788 'users' => [
1789 'a' => [
1790 'groups' => ['groupa'],
1792 'b' => [
1793 'groups' => ['groupb'],
1795 'c' => [
1796 'groups' => ['groupa', 'groupb'],
1798 'd' => [
1799 'groups' => [],
1802 'expect' => [
1803 // Tests for jointype: ANY.
1804 'ANY: No filter' => (object) [
1805 'groups' => [],
1806 'jointype' => filter::JOINTYPE_ANY,
1807 'count' => 4,
1808 'expectedusers' => [
1809 'a',
1810 'b',
1811 'c',
1812 'd',
1815 'ANY: Filter on a single group' => (object) [
1816 'groups' => ['groupa'],
1817 'jointype' => filter::JOINTYPE_ANY,
1818 'count' => 2,
1819 'expectedusers' => [
1820 'a',
1821 'c',
1824 'ANY: Filter on a group with no members' => (object) [
1825 'groups' => ['groupc'],
1826 'jointype' => filter::JOINTYPE_ANY,
1827 'count' => 0,
1828 'expectedusers' => [],
1830 'ANY: Filter on multiple groups' => (object) [
1831 'groups' => ['groupa', 'groupb'],
1832 'jointype' => filter::JOINTYPE_ANY,
1833 'count' => 3,
1834 'expectedusers' => [
1835 'a',
1836 'b',
1837 'c',
1840 'ANY: Filter on members of no groups only' => (object) [
1841 'groups' => ['nogroups'],
1842 'jointype' => filter::JOINTYPE_ANY,
1843 'count' => 1,
1844 'expectedusers' => [
1845 'd',
1848 'ANY: Filter on a single group or no groups' => (object) [
1849 'groups' => ['groupa', 'nogroups'],
1850 'jointype' => filter::JOINTYPE_ANY,
1851 'count' => 3,
1852 'expectedusers' => [
1853 'a',
1854 'c',
1855 'd',
1858 'ANY: Filter on multiple groups or no groups' => (object) [
1859 'groups' => ['groupa', 'groupb', 'nogroups'],
1860 'jointype' => filter::JOINTYPE_ANY,
1861 'count' => 4,
1862 'expectedusers' => [
1863 'a',
1864 'b',
1865 'c',
1866 'd',
1870 // Tests for jointype: ALL.
1871 'ALL: No filter' => (object) [
1872 'groups' => [],
1873 'jointype' => filter::JOINTYPE_ALL,
1874 'count' => 4,
1875 'expectedusers' => [
1876 'a',
1877 'b',
1878 'c',
1879 'd',
1882 'ALL: Filter on a single group' => (object) [
1883 'groups' => ['groupa'],
1884 'jointype' => filter::JOINTYPE_ALL,
1885 'count' => 2,
1886 'expectedusers' => [
1887 'a',
1888 'c',
1891 'ALL: Filter on a group with no members' => (object) [
1892 'groups' => ['groupc'],
1893 'jointype' => filter::JOINTYPE_ALL,
1894 'count' => 0,
1895 'expectedusers' => [],
1897 'ALL: Filter on members of no groups only' => (object) [
1898 'groups' => ['nogroups'],
1899 'jointype' => filter::JOINTYPE_ALL,
1900 'count' => 1,
1901 'expectedusers' => [
1902 'd',
1905 'ALL: Filter on multiple groups' => (object) [
1906 'groups' => ['groupa', 'groupb'],
1907 'jointype' => filter::JOINTYPE_ALL,
1908 'count' => 1,
1909 'expectedusers' => [
1910 'c',
1913 'ALL: Filter on a single group and no groups' => (object) [
1914 'groups' => ['groupa', 'nogroups'],
1915 'jointype' => filter::JOINTYPE_ALL,
1916 'count' => 0,
1917 'expectedusers' => [],
1919 'ALL: Filter on multiple groups and no groups' => (object) [
1920 'groups' => ['groupa', 'groupb', 'nogroups'],
1921 'jointype' => filter::JOINTYPE_ALL,
1922 'count' => 0,
1923 'expectedusers' => [],
1926 // Tests for jointype: NONE.
1927 'NONE: No filter' => (object) [
1928 'groups' => [],
1929 'jointype' => filter::JOINTYPE_NONE,
1930 'count' => 4,
1931 'expectedusers' => [
1932 'a',
1933 'b',
1934 'c',
1935 'd',
1938 'NONE: Filter on a single group' => (object) [
1939 'groups' => ['groupa'],
1940 'jointype' => filter::JOINTYPE_NONE,
1941 'count' => 2,
1942 'expectedusers' => [
1943 'b',
1944 'd',
1947 'NONE: Filter on a group with no members' => (object) [
1948 'groups' => ['groupc'],
1949 'jointype' => filter::JOINTYPE_NONE,
1950 'count' => 4,
1951 'expectedusers' => [
1952 'a',
1953 'b',
1954 'c',
1955 'd',
1958 'NONE: Filter on members of no groups only' => (object) [
1959 'groups' => ['nogroups'],
1960 'jointype' => filter::JOINTYPE_NONE,
1961 'count' => 3,
1962 'expectedusers' => [
1963 'a',
1964 'b',
1965 'c',
1968 'NONE: Filter on multiple groups' => (object) [
1969 'groups' => ['groupa', 'groupb'],
1970 'jointype' => filter::JOINTYPE_NONE,
1971 'count' => 1,
1972 'expectedusers' => [
1973 'd',
1976 'NONE: Filter on a single group and no groups' => (object) [
1977 'groups' => ['groupa', 'nogroups'],
1978 'jointype' => filter::JOINTYPE_NONE,
1979 'count' => 1,
1980 'expectedusers' => [
1981 'b',
1984 'NONE: Filter on multiple groups and no groups' => (object) [
1985 'groups' => ['groupa', 'groupb', 'nogroups'],
1986 'jointype' => filter::JOINTYPE_NONE,
1987 'count' => 0,
1988 'expectedusers' => [],
1994 $finaltests = [];
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,
2008 return $finaltests;
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);
2026 $users = [];
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);
2059 // Run the search.
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.
2076 * @return array
2078 public function accesssince_provider(): array {
2079 $tests = [
2080 // Users with different last access times.
2081 'Users in different groups' => (object) [
2082 'users' => [
2083 'a' => [
2084 'lastlogin' => '-3 days',
2086 'b' => [
2087 'lastlogin' => '-2 weeks',
2089 'c' => [
2090 'lastlogin' => '-5 months',
2092 'd' => [
2093 'lastlogin' => '-11 months',
2095 'e' => [
2096 // Never logged in.
2097 'lastlogin' => '',
2100 'expect' => [
2101 // Tests for jointype: ANY.
2102 'ANY: No filter' => (object) [
2103 'accesssince' => [],
2104 'jointype' => filter::JOINTYPE_ANY,
2105 'count' => 5,
2106 'expectedusers' => [
2107 'a',
2108 'b',
2109 'c',
2110 'd',
2111 'e',
2114 'ANY: Filter on last login more than 1 year ago' => (object) [
2115 'accesssince' => ['-1 year'],
2116 'jointype' => filter::JOINTYPE_ANY,
2117 'count' => 1,
2118 'expectedusers' => [
2119 'e',
2122 'ANY: Filter on last login more than 6 months ago' => (object) [
2123 'accesssince' => ['-6 months'],
2124 'jointype' => filter::JOINTYPE_ANY,
2125 'count' => 2,
2126 'expectedusers' => [
2127 'd',
2128 'e',
2131 'ANY: Filter on last login more than 3 weeks ago' => (object) [
2132 'accesssince' => ['-3 weeks'],
2133 'jointype' => filter::JOINTYPE_ANY,
2134 'count' => 3,
2135 'expectedusers' => [
2136 'c',
2137 'd',
2138 'e',
2141 'ANY: Filter on last login more than 5 days ago' => (object) [
2142 'accesssince' => ['-5 days'],
2143 'jointype' => filter::JOINTYPE_ANY,
2144 'count' => 4,
2145 'expectedusers' => [
2146 'b',
2147 'c',
2148 'd',
2149 'e',
2152 'ANY: Filter on last login more than 2 days ago' => (object) [
2153 'accesssince' => ['-2 days'],
2154 'jointype' => filter::JOINTYPE_ANY,
2155 'count' => 5,
2156 'expectedusers' => [
2157 'a',
2158 'b',
2159 'c',
2160 'd',
2161 'e',
2165 // Tests for jointype: ALL.
2166 'ALL: No filter' => (object) [
2167 'accesssince' => [],
2168 'jointype' => filter::JOINTYPE_ALL,
2169 'count' => 5,
2170 'expectedusers' => [
2171 'a',
2172 'b',
2173 'c',
2174 'd',
2175 'e',
2178 'ALL: Filter on last login more than 1 year ago' => (object) [
2179 'accesssince' => ['-1 year'],
2180 'jointype' => filter::JOINTYPE_ALL,
2181 'count' => 1,
2182 'expectedusers' => [
2183 'e',
2186 'ALL: Filter on last login more than 6 months ago' => (object) [
2187 'accesssince' => ['-6 months'],
2188 'jointype' => filter::JOINTYPE_ALL,
2189 'count' => 2,
2190 'expectedusers' => [
2191 'd',
2192 'e',
2195 'ALL: Filter on last login more than 3 weeks ago' => (object) [
2196 'accesssince' => ['-3 weeks'],
2197 'jointype' => filter::JOINTYPE_ALL,
2198 'count' => 3,
2199 'expectedusers' => [
2200 'c',
2201 'd',
2202 'e',
2205 'ALL: Filter on last login more than 5 days ago' => (object) [
2206 'accesssince' => ['-5 days'],
2207 'jointype' => filter::JOINTYPE_ALL,
2208 'count' => 4,
2209 'expectedusers' => [
2210 'b',
2211 'c',
2212 'd',
2213 'e',
2216 'ALL: Filter on last login more than 2 days ago' => (object) [
2217 'accesssince' => ['-2 days'],
2218 'jointype' => filter::JOINTYPE_ALL,
2219 'count' => 5,
2220 'expectedusers' => [
2221 'a',
2222 'b',
2223 'c',
2224 'd',
2225 'e',
2229 // Tests for jointype: NONE.
2230 'NONE: No filter' => (object) [
2231 'accesssince' => [],
2232 'jointype' => filter::JOINTYPE_NONE,
2233 'count' => 5,
2234 'expectedusers' => [
2235 'a',
2236 'b',
2237 'c',
2238 'd',
2239 'e',
2242 'NONE: Filter on last login more than 1 year ago' => (object) [
2243 'accesssince' => ['-1 year'],
2244 'jointype' => filter::JOINTYPE_NONE,
2245 'count' => 4,
2246 'expectedusers' => [
2247 'a',
2248 'b',
2249 'c',
2250 'd',
2253 'NONE: Filter on last login more than 6 months ago' => (object) [
2254 'accesssince' => ['-6 months'],
2255 'jointype' => filter::JOINTYPE_NONE,
2256 'count' => 3,
2257 'expectedusers' => [
2258 'a',
2259 'b',
2260 'c',
2263 'NONE: Filter on last login more than 3 weeks ago' => (object) [
2264 'accesssince' => ['-3 weeks'],
2265 'jointype' => filter::JOINTYPE_NONE,
2266 'count' => 2,
2267 'expectedusers' => [
2268 'a',
2269 'b',
2272 'NONE: Filter on last login more than 5 days ago' => (object) [
2273 'accesssince' => ['-5 days'],
2274 'jointype' => filter::JOINTYPE_NONE,
2275 'count' => 1,
2276 'expectedusers' => [
2277 'a',
2280 'NONE: Filter on last login more than 2 days ago' => (object) [
2281 'accesssince' => ['-2 days'],
2282 'jointype' => filter::JOINTYPE_NONE,
2283 'count' => 0,
2284 'expectedusers' => [],
2290 $finaltests = [];
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,
2303 return $finaltests;
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 {
2319 global $DB;
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');
2330 $users = [];
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) {
2349 $groupinfo = [
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) {
2376 $userinfo = [
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']);
2464 // Run the search.
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.
2481 * @return array
2483 public function filterset_joins_provider(): array {
2484 $tests = [
2485 // Users with different configurations.
2486 'Users with different configurations' => (object) [
2487 'groupsavailable' => [
2488 'groupa',
2489 'groupb',
2490 'groupc',
2492 'users' => [
2493 'adam.ant' => [
2494 'firstname' => 'Adam',
2495 'lastname' => 'Ant',
2496 'enrolments' => [
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',
2509 'enrolments' => [
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',
2527 'enrolments' => [
2529 'role' => 'editingteacher',
2530 'method' => 'self',
2531 'status' => ENROL_USER_SUSPENDED,
2534 'groups' => ['groupa', 'groupb'],
2535 'lastlogin' => '-5 months',
2537 'tony.rogers' => [
2538 'firstname' => 'Anthony',
2539 'lastname' => 'Rogers',
2540 'enrolments' => [
2542 'role' => 'editingteacher',
2543 'method' => 'self',
2544 'status' => ENROL_USER_SUSPENDED,
2547 'groups' => [],
2548 'lastlogin' => '-10 months',
2550 'sarah.rester' => [
2551 'firstname' => 'Sarah',
2552 'lastname' => 'Rester',
2553 'email' => 'zazu@example.com',
2554 'enrolments' => [
2556 'role' => 'teacher',
2557 'method' => 'manual',
2558 'status' => ENROL_USER_ACTIVE,
2561 'role' => 'editingteacher',
2562 'method' => 'self',
2563 'status' => ENROL_USER_SUSPENDED,
2566 'groups' => [],
2567 'lastlogin' => '-11 months',
2569 'morgan.crikeyson' => [
2570 'firstname' => 'Morgan',
2571 'lastname' => 'Crikeyson',
2572 'enrolments' => [
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',
2585 'enrolments' => [
2587 'role' => 'student',
2588 'method' => 'manual',
2589 'status' => ENROL_USER_ACTIVE,
2592 'groups' => [],
2593 // Never logged in.
2594 'lastlogin' => '',
2597 'expect' => [
2598 // Tests for jointype: ANY.
2599 'ANY: No filters in filterset' => (object) [
2600 'filterdata' => [],
2601 'jointype' => filter::JOINTYPE_ANY,
2602 'count' => 7,
2603 'expectedusers' => [
2604 'adam.ant',
2605 'barbara.bennett',
2606 'colin.carnforth',
2607 'tony.rogers',
2608 'sarah.rester',
2609 'morgan.crikeyson',
2610 'jonathan.bravo',
2613 'ANY: Filterset containing a single filter type' => (object) [
2614 'filterdata' => [
2615 'enrolmethods' => [
2616 'values' => ['self'],
2617 'jointype' => filter::JOINTYPE_ANY,
2620 'jointype' => filter::JOINTYPE_ANY,
2621 'count' => 3,
2622 'expectedusers' => [
2623 'colin.carnforth',
2624 'tony.rogers',
2625 'sarah.rester',
2628 'ANY: Filterset matching all filter types on different users' => (object) [
2629 'filterdata' => [
2630 // Match Adam only.
2631 'keywords' => [
2632 'values' => ['adam'],
2633 'jointype' => filter::JOINTYPE_ALL,
2635 // Match Sarah only.
2636 'enrolmethods' => [
2637 'values' => ['manual', 'self'],
2638 'jointype' => filter::JOINTYPE_ALL,
2640 // Match Barbara only.
2641 'courseroles' => [
2642 'values' => ['student', 'teacher'],
2643 'jointype' => filter::JOINTYPE_ALL,
2645 // Match Sarah only.
2646 'statuses' => [
2647 'values' => ['active', 'suspended'],
2648 'jointype' => filter::JOINTYPE_ALL,
2650 // Match Colin only.
2651 'groups' => [
2652 'values' => ['groupa', 'groupb'],
2653 'jointype' => filter::JOINTYPE_ALL,
2655 // Match Jonathan only.
2656 'accesssince' => [
2657 'values' => ['-1 year'],
2658 'jointype' => filter::JOINTYPE_ALL,
2661 'jointype' => filter::JOINTYPE_ANY,
2662 'count' => 5,
2663 // Morgan and Tony are not matched, to confirm filtering is not just returning all users.
2664 'expectedusers' => [
2665 'adam.ant',
2666 'barbara.bennett',
2667 'colin.carnforth',
2668 'sarah.rester',
2669 'jonathan.bravo',
2673 // Tests for jointype: ALL.
2674 'ALL: No filters in filterset' => (object) [
2675 'filterdata' => [],
2676 'jointype' => filter::JOINTYPE_ALL,
2677 'count' => 7,
2678 'expectedusers' => [
2679 'adam.ant',
2680 'barbara.bennett',
2681 'colin.carnforth',
2682 'tony.rogers',
2683 'sarah.rester',
2684 'morgan.crikeyson',
2685 'jonathan.bravo',
2688 'ALL: Filterset containing a single filter type' => (object) [
2689 'filterdata' => [
2690 'enrolmethods' => [
2691 'values' => ['self'],
2692 'jointype' => filter::JOINTYPE_ANY,
2695 'jointype' => filter::JOINTYPE_ALL,
2696 'count' => 3,
2697 'expectedusers' => [
2698 'colin.carnforth',
2699 'tony.rogers',
2700 'sarah.rester',
2703 'ALL: Filterset combining all filter types' => (object) [
2704 'filterdata' => [
2705 // Exclude Adam, Tony, Morgan and Jonathan.
2706 'keywords' => [
2707 'values' => ['ar'],
2708 'jointype' => filter::JOINTYPE_ANY,
2710 // Exclude Colin and Tony.
2711 'enrolmethods' => [
2712 'values' => ['manual'],
2713 'jointype' => filter::JOINTYPE_ANY,
2715 // Exclude Adam, Barbara and Jonathan.
2716 'courseroles' => [
2717 'values' => ['student'],
2718 'jointype' => filter::JOINTYPE_NONE,
2720 // Exclude Colin and Tony.
2721 'statuses' => [
2722 'values' => ['active'],
2723 'jointype' => filter::JOINTYPE_ALL,
2725 // Exclude Barbara.
2726 'groups' => [
2727 'values' => ['groupa', 'nogroups'],
2728 'jointype' => filter::JOINTYPE_ANY,
2730 // Exclude Adam, Colin and Barbara.
2731 'accesssince' => [
2732 'values' => ['-6 months'],
2733 'jointype' => filter::JOINTYPE_ALL,
2736 'jointype' => filter::JOINTYPE_ALL,
2737 'count' => 1,
2738 'expectedusers' => [
2739 'sarah.rester',
2743 // Tests for jointype: NONE.
2744 'NONE: No filters in filterset' => (object) [
2745 'filterdata' => [],
2746 'jointype' => filter::JOINTYPE_NONE,
2747 'count' => 7,
2748 'expectedusers' => [
2749 'adam.ant',
2750 'barbara.bennett',
2751 'colin.carnforth',
2752 'tony.rogers',
2753 'sarah.rester',
2754 'morgan.crikeyson',
2755 'jonathan.bravo',
2758 'NONE: Filterset containing a single filter type' => (object) [
2759 'filterdata' => [
2760 'enrolmethods' => [
2761 'values' => ['self'],
2762 'jointype' => filter::JOINTYPE_ANY,
2765 'jointype' => filter::JOINTYPE_NONE,
2766 'count' => 4,
2767 'expectedusers' => [
2768 'adam.ant',
2769 'barbara.bennett',
2770 'morgan.crikeyson',
2771 'jonathan.bravo',
2774 'NONE: Filterset combining all filter types' => (object) [
2775 'filterdata' => [
2776 // Excludes Adam.
2777 'keywords' => [
2778 'values' => ['adam'],
2779 'jointype' => filter::JOINTYPE_ANY,
2781 // Excludes Colin, Tony and Sarah.
2782 'enrolmethods' => [
2783 'values' => ['self'],
2784 'jointype' => filter::JOINTYPE_ANY,
2786 // Excludes Jonathan.
2787 'courseroles' => [
2788 'values' => ['student'],
2789 'jointype' => filter::JOINTYPE_NONE,
2791 // Excludes Colin, Tony and Sarah.
2792 'statuses' => [
2793 'values' => ['suspended'],
2794 'jointype' => filter::JOINTYPE_ALL,
2796 // Excludes Adam, Colin, Tony, Sarah, Morgan and Jonathan.
2797 'groups' => [
2798 'values' => ['groupa', 'nogroups'],
2799 'jointype' => filter::JOINTYPE_ANY,
2801 // Excludes Tony and Sarah.
2802 'accesssince' => [
2803 'values' => ['-6 months'],
2804 'jointype' => filter::JOINTYPE_ALL,
2807 'jointype' => filter::JOINTYPE_NONE,
2808 'count' => 1,
2809 'expectedusers' => [
2810 'barbara.bennett',
2817 $finaltests = [];
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,
2831 return $finaltests;