66ce32c6bc7a7ecb806d4d8f9bb96dda3eb116ea
[moodle.git] / user / tests / table / participants_search_test.php
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.
13 //
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.
19  *
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
24  */
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.
43  *
44  * @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
45  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
46  */
47 class participants_search_test extends advanced_testcase {
49     /**
50      * Helper to convert a moodle_recordset to an array of records.
51      *
52      * @param moodle_recordset $recordset
53      * @return array
54      */
55     protected function convert_recordset_to_array(moodle_recordset $recordset): array {
56         $records = [];
57         foreach ($recordset as $record) {
58             $records[$record->id] = $record;
59         }
60         $recordset->close();
62         return $records;
63     }
65     /**
66      * Create and enrol a set of users into the specified course.
67      *
68      * @param stdClass $course
69      * @param int $count
70      * @param null|string $role
71      * @return array
72      */
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;
81         }
83         return $users;
84     }
86     /**
87      * Create a new course with several types of user.
88      *
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
94      */
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' => [],
102         ];
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;
112     }
113     /**
114      * Ensure that the roles filter works as expected with the provided test cases.
115      *
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
122      */
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);
146                 }
147             }
149             if (array_key_exists('categoryroles', $userdata)) {
150                 foreach ($userdata['categoryroles'] as $rolename) {
151                     $this->getDataGenerator()->role_assign($roles[$rolename], $user->id, $categorycontext->id);
152                 }
153             }
154             $users[$username] = $user;
155         }
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]);
171         }
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);
185         }
186     }
188     /**
189      * Data provider for role tests.
190      *
191      * @return array
192      */
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',
201                         ],
202                     ],
203                     'b' => [
204                         'courseroles' => [
205                             'student',
206                         ],
207                     ],
208                     'c' => [
209                         'courseroles' => [
210                             'editingteacher',
211                         ],
212                     ],
213                     'd' => [
214                         'courseroles' => [
215                             'editingteacher',
216                         ],
217                     ],
218                     'e' => [
219                         'courseroles' => [
220                             'teacher',
221                         ],
222                     ],
223                     'f' => [
224                         'courseroles' => [
225                             'teacher',
226                         ],
227                     ],
228                     // User is enrolled in the course without role.
229                     'g' => [
230                         'courseroles' => [
231                         ],
232                     ],
234                     // User is a category manager and also enrolled without role in the course.
235                     'h' => [
236                         'courseroles' => [
237                         ],
238                         'categoryroles' => [
239                             'manager',
240                         ],
241                     ],
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',
248                         ],
249                     ],
250                 ],
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',
266                         ],
267                     ],
268                     'ANY: Filter on student' => (object) [
269                         'roles' => ['student'],
270                         'jointype' => filter::JOINTYPE_ANY,
271                         'count' => 2,
272                         'expectedusers' => [
273                             'a',
274                             'b',
275                         ],
276                     ],
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',
286                         ],
287                     ],
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',
296                         ],
297                     ],
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',
305                         ],
306                     ],
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',
322                         ],
323                     ],
324                     'ALL: Filter on student' => (object) [
325                         'roles' => ['student'],
326                         'jointype' => filter::JOINTYPE_ALL,
327                         'count' => 2,
328                         'expectedusers' => [
329                             'a',
330                             'b',
331                         ],
332                     ],
333                     'ALL: Filter on student, teacher' => (object) [
334                         'roles' => ['student', 'teacher'],
335                         'jointype' => filter::JOINTYPE_ALL,
336                         'count' => 0,
337                         'expectedusers' => [],
338                     ],
339                     'ALL: Filter on student, manager (category level role))' => (object) [
340                         'roles' => ['student', 'manager'],
341                         'jointype' => filter::JOINTYPE_ALL,
342                         'count' => 0,
343                         'expectedusers' => [],
344                     ],
345                     'ALL: Filter on student, coursecreator (not assigned))' => (object) [
346                         'roles' => ['student', 'coursecreator'],
347                         'jointype' => filter::JOINTYPE_ALL,
348                         'count' => 0,
349                         'expectedusers' => [],
350                     ],
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',
366                         ],
367                     ],
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',
379                         ],
380                     ],
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',
390                         ],
391                     ],
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',
402                         ],
403                     ],
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',
415                         ],
416                     ],
417                 ],
418             ],
419             'Users with multiple roles' => (object) [
420                 'users' => [
421                     'a' => [
422                         'courseroles' => [
423                             'student',
424                         ],
425                     ],
426                     'b' => [
427                         'courseroles' => [
428                             'student',
429                             'teacher',
430                         ],
431                     ],
432                     'c' => [
433                         'courseroles' => [
434                             'editingteacher',
435                         ],
436                     ],
437                     'd' => [
438                         'courseroles' => [
439                             'editingteacher',
440                         ],
441                     ],
442                     'e' => [
443                         'courseroles' => [
444                             'teacher',
445                             'editingteacher',
446                         ],
447                     ],
448                     'f' => [
449                         'courseroles' => [
450                             'teacher',
451                         ],
452                     ],
454                     // User is enrolled in the course without role.
455                     'g' => [
456                         'courseroles' => [
457                         ],
458                     ],
460                     // User is a category manager and also enrolled without role in the course.
461                     'h' => [
462                         'courseroles' => [
463                         ],
464                         'categoryroles' => [
465                             'manager',
466                         ],
467                     ],
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',
474                         ],
475                     ],
476                 ],
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',
492                         ],
493                     ],
494                     'ANY: Filter on student' => (object) [
495                         'roles' => ['student'],
496                         'jointype' => filter::JOINTYPE_ANY,
497                         'count' => 2,
498                         'expectedusers' => [
499                             'a',
500                             'b',
501                         ],
502                     ],
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',
511                         ],
512                     ],
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',
521                         ],
522                     ],
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',
532                         ],
533                     ],
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',
544                         ],
545                     ],
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',
554                         ],
555                     ],
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',
563                         ],
564                     ],
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',
580                         ],
581                     ],
582                     'ALL: Filter on student' => (object) [
583                         'roles' => ['student'],
584                         'jointype' => filter::JOINTYPE_ALL,
585                         'count' => 2,
586                         'expectedusers' => [
587                             'a',
588                             'b',
589                         ],
590                     ],
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',
599                         ],
600                     ],
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',
609                         ],
610                     ],
611                     'ALL: Filter on student, teacher' => (object) [
612                         'roles' => ['student', 'teacher'],
613                         'jointype' => filter::JOINTYPE_ALL,
614                         'count' => 1,
615                         'expectedusers' => [
616                             'b',
617                         ],
618                     ],
619                     'ALL: Filter on teacher, editingteacher' => (object) [
620                         'roles' => ['teacher', 'editingteacher'],
621                         'jointype' => filter::JOINTYPE_ALL,
622                         'count' => 1,
623                         'expectedusers' => [
624                             'e',
625                         ],
626                     ],
627                     'ALL: Filter on student, manager (category level role)' => (object) [
628                         'roles' => ['student', 'manager'],
629                         'jointype' => filter::JOINTYPE_ALL,
630                         'count' => 0,
631                         'expectedusers' => [],
632                     ],
633                     'ALL: Filter on student, coursecreator (not assigned)' => (object) [
634                         'roles' => ['student', 'coursecreator'],
635                         'jointype' => filter::JOINTYPE_ALL,
636                         'count' => 0,
637                         'expectedusers' => [],
638                     ],
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',
654                         ],
655                     ],
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',
667                         ],
668                     ],
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',
679                         ],
680                     ],
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',
691                         ],
692                     ],
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',
703                         ],
704                     ],
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',
713                         ],
714                     ],
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',
725                         ],
726                     ],
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',
738                         ],
739                     ],
740                 ],
741             ],
742         ];
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,
753                 ];
754             }
755         }
757         return $finaltests;
758     }
760     /**
761      * Ensure that the keywords filter works as expected with the provided test cases.
762      *
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
769      */
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;
785         }
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);
801         }
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);
815         }
816     }
818     /**
819      * Data provider for keywords tests.
820      *
821      * @return array
822      */
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',
831                     ],
832                     'barbara.bennett' => [
833                         'firstname' => 'Barbara',
834                         'lastname' => 'Bennett',
835                         'alternatename' => 'Babs',
836                         'firstnamephonetic' => 'Barbra',
837                         'lastnamephonetic' => 'Benit',
838                     ],
839                     'colin.carnforth' => [
840                         'firstname' => 'Colin',
841                         'lastname' => 'Carnforth',
842                         'middlename' => 'Jeffery',
843                     ],
844                     'tony.rogers' => [
845                         'firstname' => 'Anthony',
846                         'lastname' => 'Rogers',
847                         'lastnamephonetic' => 'Rowjours',
848                     ],
849                     'sarah.rester' => [
850                         'firstname' => 'Sarah',
851                         'lastname' => 'Rester',
852                         'email' => 'zazu@example.com',
853                         'firstnamephonetic' => 'Sera',
854                     ],
855                 ],
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',
868                         ],
869                     ],
870                     'ANY: Filter on first name only' => (object) [
871                         'keywords' => ['adam'],
872                         'jointype' => filter::JOINTYPE_ANY,
873                         'count' => 1,
874                         'expectedusers' => [
875                             'adam.ant',
876                         ],
877                     ],
878                     'ANY: Filter on last name only' => (object) [
879                         'keywords' => ['BeNNeTt'],
880                         'jointype' => filter::JOINTYPE_ANY,
881                         'count' => 1,
882                         'expectedusers' => [
883                             'barbara.bennett',
884                         ],
885                     ],
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',
893                         ],
894                     ],
895                     'ANY: Filter on middlename only' => (object) [
896                         'keywords' => ['Jeff'],
897                         'jointype' => filter::JOINTYPE_ANY,
898                         'count' => 1,
899                         'expectedusers' => [
900                             'colin.carnforth',
901                         ],
902                     ],
903                     'ANY: Filter on username (no match)' => (object) [
904                         'keywords' => ['sara.rester'],
905                         'jointype' => filter::JOINTYPE_ANY,
906                         'count' => 0,
907                         'expectedusers' => [],
908                     ],
909                     'ANY: Filter on email only' => (object) [
910                         'keywords' => ['zazu'],
911                         'jointype' => filter::JOINTYPE_ANY,
912                         'count' => 1,
913                         'expectedusers' => [
914                             'sarah.rester',
915                         ],
916                     ],
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',
923                         ],
924                     ],
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',
931                         ],
932                     ],
933                     'ANY: Filter on alternate name only' => (object) [
934                         'keywords' => ['Babs'],
935                         'jointype' => filter::JOINTYPE_ANY,
936                         'count' => 1,
937                         'expectedusers' => [
938                             'barbara.bennett',
939                         ],
940                     ],
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',
949                         ],
950                     ],
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',
959                         ],
960                     ],
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',
973                         ],
974                     ],
975                     'ALL: Filter on first name only' => (object) [
976                         'keywords' => ['adam'],
977                         'jointype' => filter::JOINTYPE_ALL,
978                         'count' => 1,
979                         'expectedusers' => [
980                             'adam.ant',
981                         ],
982                     ],
983                     'ALL: Filter on last name only' => (object) [
984                         'keywords' => ['BeNNeTt'],
985                         'jointype' => filter::JOINTYPE_ALL,
986                         'count' => 1,
987                         'expectedusers' => [
988                             'barbara.bennett',
989                         ],
990                     ],
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',
998                         ],
999                     ],
1000                     'ALL: Filter on middlename only' => (object) [
1001                         'keywords' => ['Jeff'],
1002                         'jointype' => filter::JOINTYPE_ALL,
1003                         'count' => 1,
1004                         'expectedusers' => [
1005                             'colin.carnforth',
1006                         ],
1007                     ],
1008                     'ALL: Filter on username (no match)' => (object) [
1009                         'keywords' => ['sara.rester'],
1010                         'jointype' => filter::JOINTYPE_ALL,
1011                         'count' => 0,
1012                         'expectedusers' => [],
1013                     ],
1014                     'ALL: Filter on email only' => (object) [
1015                         'keywords' => ['zazu'],
1016                         'jointype' => filter::JOINTYPE_ALL,
1017                         'count' => 1,
1018                         'expectedusers' => [
1019                             'sarah.rester',
1020                         ],
1021                     ],
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',
1028                         ],
1029                     ],
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',
1036                         ],
1037                     ],
1038                     'ALL: Filter on alternate name only' => (object) [
1039                         'keywords' => ['Babs'],
1040                         'jointype' => filter::JOINTYPE_ALL,
1041                         'count' => 1,
1042                         'expectedusers' => [
1043                             'barbara.bennett',
1044                         ],
1045                     ],
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',
1052                         ],
1053                     ],
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' => [],
1059                     ],
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',
1066                         ],
1067                     ],
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',
1080                         ],
1081                     ],
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',
1090                         ],
1091                     ],
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',
1101                         ],
1102                     ],
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',
1110                         ],
1111                     ],
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',
1121                         ],
1122                     ],
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',
1133                         ],
1134                     ],
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',
1144                         ],
1145                     ],
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',
1155                         ],
1156                     ],
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',
1166                         ],
1167                     ],
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',
1177                         ],
1178                     ],
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',
1186                         ],
1187                     ],
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',
1195                         ],
1196                     ],
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',
1206                         ],
1207                     ],
1208                 ],
1209             ],
1210         ];
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,
1221                 ];
1222             }
1223         }
1225         return $finaltests;
1226     }
1228     /**
1229      * Ensure that the enrolment status filter works as expected with the provided test cases.
1230      *
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
1237      */
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);
1251         }
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);
1259                 }
1260             }
1262             $users[$username] = $user;
1263         }
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);
1279         }
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);
1293         }
1294     }
1296     /**
1297      * Data provider for status filter tests.
1298      *
1299      * @return array
1300      */
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,
1309                         ]
1310                     ],
1311                     'b' => [
1312                         'statuses' => [
1313                             'self' => ENROL_USER_ACTIVE,
1314                         ]
1315                     ],
1316                     'c' => [
1317                         'statuses' => [
1318                             'manual' => ENROL_USER_SUSPENDED,
1319                         ]
1320                     ],
1321                     'd' => [
1322                         'statuses' => [
1323                             'self' => ENROL_USER_SUSPENDED,
1324                         ]
1325                     ],
1326                     'e' => [
1327                         'statuses' => [
1328                             'manual' => ENROL_USER_ACTIVE,
1329                             'self' => ENROL_USER_SUSPENDED,
1330                         ]
1331                     ],
1332                 ],
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',
1345                         ],
1346                     ],
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',
1355                         ],
1356                     ],
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',
1365                         ],
1366                     ],
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',
1377                         ],
1378                     ],
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',
1391                         ],
1392                     ],
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',
1401                         ],
1402                     ],
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',
1411                         ],
1412                     ],
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',
1419                         ],
1420                     ],
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',
1433                         ],
1434                     ],
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',
1443                         ],
1444                     ],
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',
1453                         ],
1454                     ],
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' => [],
1460                     ],
1461                 ],
1462             ],
1463         ];
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,
1474                 ];
1475             }
1476         }
1478         return $finaltests;
1479     }
1481     /**
1482      * Ensure that the enrolment methods filter works as expected with the provided test cases.
1483      *
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
1490      */
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;
1506         }
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);
1514                 }
1515             }
1517             $users[$username] = $user;
1518         }
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]);
1534         }
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);
1548         }
1549     }
1551     /**
1552      * Data provider for enrolments filter tests.
1553      *
1554      * @return array
1555      */
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',
1564                         ]
1565                     ],
1566                     'b' => [
1567                         'enrolmethods' => [
1568                             'self',
1569                         ]
1570                     ],
1571                     'c' => [
1572                         'enrolmethods' => [
1573                             'manual',
1574                             'self',
1575                         ]
1576                     ],
1577                 ],
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',
1588                         ],
1589                     ],
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',
1597                         ],
1598                     ],
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',
1606                         ],
1607                     ],
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',
1616                         ],
1617                     ],
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',
1628                         ],
1629                     ],
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',
1637                         ],
1638                     ],
1639                     'ALL: Filter by multiple enrolment methods' => (object) [
1640                         'enrolmethods' => ['manual', 'self'],
1641                         'jointype' => filter::JOINTYPE_ALL,
1642                         'count' => 1,
1643                         'expectedusers' => [
1644                             'c',
1645                         ],
1646                     ],
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',
1657                         ],
1658                     ],
1659                     'NONE: Filter by manual enrolments only' => (object) [
1660                         'enrolmethods' => ['manual'],
1661                         'jointype' => filter::JOINTYPE_NONE,
1662                         'count' => 1,
1663                         'expectedusers' => [
1664                             'b',
1665                         ],
1666                     ],
1667                     'NONE: Filter by multiple enrolment methods' => (object) [
1668                         'enrolmethods' => ['manual', 'self'],
1669                         'jointype' => filter::JOINTYPE_NONE,
1670                         'count' => 0,
1671                         'expectedusers' => [],
1672                     ],
1673                 ],
1674             ],
1675         ];
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,
1686                 ];
1687             }
1688         }
1690         return $finaltests;
1691     }
1693     /**
1694      * Ensure that the groups filter works as expected with the provided test cases.
1695      *
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
1703      */
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,
1714         ];
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,
1722             ];
1724             $groupsdata[$groupname] = $this->getDataGenerator()->create_group($groupinfo);
1725         }
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,
1736                     ];
1737                     $this->getDataGenerator()->create_group_member($userinfo);
1738                 }
1739             }
1741             $users[$username] = $user;
1742         }
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);
1758         }
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);
1772         }
1773     }
1775     /**
1776      * Data provider for groups filter tests.
1777      *
1778      * @return array
1779      */
1780     public function groups_provider(): array {
1781         $tests = [
1782             'Users in different groups' => (object) [
1783                 'groupsavailable' => [
1784                     'groupa',
1785                     'groupb',
1786                     'groupc',
1787                 ],
1788                 'users' => [
1789                     'a' => [
1790                         'groups' => ['groupa'],
1791                     ],
1792                     'b' => [
1793                         'groups' => ['groupb'],
1794                     ],
1795                     'c' => [
1796                         'groups' => ['groupa', 'groupb'],
1797                     ],
1798                     'd' => [
1799                         'groups' => [],
1800                     ],
1801                 ],
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',
1813                         ],
1814                     ],
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',
1822                         ],
1823                     ],
1824                     'ANY: Filter on a group with no members' => (object) [
1825                         'groups' => ['groupc'],
1826                         'jointype' => filter::JOINTYPE_ANY,
1827                         'count' => 0,
1828                         'expectedusers' => [],
1829                     ],
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',
1838                         ],
1839                     ],
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',
1846                         ],
1847                     ],
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',
1856                         ],
1857                     ],
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',
1867                         ],
1868                     ],
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',
1880                         ],
1881                     ],
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',
1889                         ],
1890                     ],
1891                     'ALL: Filter on a group with no members' => (object) [
1892                         'groups' => ['groupc'],
1893                         'jointype' => filter::JOINTYPE_ALL,
1894                         'count' => 0,
1895                         'expectedusers' => [],
1896                     ],
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',
1903                         ],
1904                     ],
1905                     'ALL: Filter on multiple groups' => (object) [
1906                         'groups' => ['groupa', 'groupb'],
1907                         'jointype' => filter::JOINTYPE_ALL,
1908                         'count' => 1,
1909                         'expectedusers' => [
1910                             'c',
1911                         ],
1912                     ],
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' => [],
1918                     ],
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' => [],
1924                     ],
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',
1936                         ],
1937                     ],
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',
1945                         ],
1946                     ],
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',
1956                         ],
1957                     ],
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',
1966                         ],
1967                     ],
1968                     'NONE: Filter on multiple groups' => (object) [
1969                         'groups' => ['groupa', 'groupb'],
1970                         'jointype' => filter::JOINTYPE_NONE,
1971                         'count' => 1,
1972                         'expectedusers' => [
1973                             'd',
1974                         ],
1975                     ],
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',
1982                         ],
1983                     ],
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' => [],
1989                     ],
1990                 ],
1991             ],
1992         ];
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,
2004                 ];
2005             }
2006         }
2008         return $finaltests;
2009     }
2011     /**
2012      * Ensure that the groups filter works as expected when separate groups mode is enabled, with the provided test cases.
2013      *
2014      * @param array $usersdata The list of users to create
2015      * @param array $groupsavailable The names of groups that should be created in the course
2016      * @param array $filtergroups The names of groups to filter by
2017      * @param int $jointype The join type to use when combining filter values
2018      * @param int $count The expected count
2019      * @param array $expectedusers
2020      * @param string $loginusername The user to login as for the tests
2021      * @dataProvider groups_separate_provider
2022      */
2023     public function test_groups_filter_separate_groups(array $usersdata, array $groupsavailable, array $filtergroups, int $jointype,
2024             int $count, array $expectedusers, string $loginusername): void {
2026         $course = $this->getDataGenerator()->create_course();
2027         $coursecontext = context_course::instance($course->id);
2028         $users = [];
2030         // Enable separate groups mode on the course.
2031         $course->groupmode = SEPARATEGROUPS;
2032         $course->groupmodeforce = true;
2033         update_course($course);
2035         // Prepare data for filtering by users in no groups.
2036         $nogroupsdata = (object) [
2037             'id' => USERSWITHOUTGROUP,
2038         ];
2040         // Map group names to group data.
2041          $groupsdata = ['nogroups' => $nogroupsdata];
2042         foreach ($groupsavailable as $groupname) {
2043             $groupinfo = [
2044                 'courseid' => $course->id,
2045                 'name' => $groupname,
2046             ];
2048             $groupsdata[$groupname] = $this->getDataGenerator()->create_group($groupinfo);
2049         }
2051         foreach ($usersdata as $username => $userdata) {
2052             $user = $this->getDataGenerator()->create_user(['username' => $username]);
2053             $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
2055             if (array_key_exists('groups', $userdata)) {
2056                 foreach ($userdata['groups'] as $groupname) {
2057                     $userinfo = [
2058                         'userid' => $user->id,
2059                         'groupid' => (int) $groupsdata[$groupname]->id,
2060                     ];
2061                     $this->getDataGenerator()->create_group_member($userinfo);
2062                 }
2063             }
2065             $users[$username] = $user;
2067             if ($username == $loginusername) {
2068                 $loginuser = $user;
2069             }
2070         }
2072         // Create a secondary course with users. We should not see these users.
2073         $this->create_course_with_users(1, 1, 1, 1);
2075         // Log in as the user to be tested.
2076         $this->setUser($loginuser);
2078         // Create the basic filter.
2079         $filterset = new participants_filterset();
2080         $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id]));
2082         // Create the groups filter.
2083         $groupsfilter = new integer_filter('groups');
2084         $filterset->add_filter($groupsfilter);
2086         // Configure the filter.
2087         foreach ($filtergroups as $filtergroupname) {
2088             $groupsfilter->add_filter_value((int) $groupsdata[$filtergroupname]->id);
2089         }
2090         $groupsfilter->set_join_type($jointype);
2092         // Run the search.
2093         $search = new participants_search($course, $coursecontext, $filterset);
2095         // Tests on user in no groups should throw an exception as they are not supported (participants are not visible to them).
2096         if (in_array('exception', $expectedusers)) {
2097             $this->expectException(\coding_exception::class);
2098             $rs = $search->get_participants();
2099         } else {
2100             // All other cases are tested as normal.
2101             $rs = $search->get_participants();
2102             $this->assertInstanceOf(moodle_recordset::class, $rs);
2103             $records = $this->convert_recordset_to_array($rs);
2105             $this->assertCount($count, $records);
2106             $this->assertEquals($count, $search->get_total_participants_count());
2108             foreach ($expectedusers as $expecteduser) {
2109                 $this->assertArrayHasKey($users[$expecteduser]->id, $records);
2110             }
2111         }
2112     }
2114     /**
2115      * Data provider for groups filter tests.
2116      *
2117      * @return array
2118      */
2119     public function groups_separate_provider(): array {
2120         $tests = [
2121             'Users in different groups with separate groups mode enabled' => (object) [
2122                 'groupsavailable' => [
2123                     'groupa',
2124                     'groupb',
2125                     'groupc',
2126                 ],
2127                 'users' => [
2128                     'a' => [
2129                         'groups' => ['groupa'],
2130                     ],
2131                     'b' => [
2132                         'groups' => ['groupb'],
2133                     ],
2134                     'c' => [
2135                         'groups' => ['groupa', 'groupb'],
2136                     ],
2137                     'd' => [
2138                         'groups' => [],
2139                     ],
2140                 ],
2141                 'expect' => [
2142                     // Tests for jointype: ANY.
2143                     'ANY: No filter, user in one group' => (object) [
2144                         'loginuser' => 'a',
2145                         'groups' => [],
2146                         'jointype' => filter::JOINTYPE_ANY,
2147                         'count' => 2,
2148                         'expectedusers' => [
2149                             'a',
2150                             'c',
2151                         ],
2152                     ],
2153                     'ANY: No filter, user in multiple groups' => (object) [
2154                         'loginuser' => 'c',
2155                         'groups' => [],
2156                         'jointype' => filter::JOINTYPE_ANY,
2157                         'count' => 3,
2158                         'expectedusers' => [
2159                             'a',
2160                             'b',
2161                             'c',
2162                         ],
2163                     ],
2164                     'ANY: No filter, user in no groups' => (object) [
2165                         'loginuser' => 'd',
2166                         'groups' => [],
2167                         'jointype' => filter::JOINTYPE_ANY,
2168                         'count' => 0,
2169                         'expectedusers' => ['exception'],
2170                     ],
2171                     'ANY: Filter on a single group, user in one group' => (object) [
2172                         'loginuser' => 'a',
2173                         'groups' => ['groupa'],
2174                         'jointype' => filter::JOINTYPE_ANY,
2175                         'count' => 2,
2176                         'expectedusers' => [
2177                             'a',
2178                             'c',
2179                         ],
2180                     ],
2181                     'ANY: Filter on a single group, user in multple groups' => (object) [
2182                         'loginuser' => 'c',
2183                         'groups' => ['groupa'],
2184                         'jointype' => filter::JOINTYPE_ANY,
2185                         'count' => 2,
2186                         'expectedusers' => [
2187                             'a',
2188                             'c',
2189                         ],
2190                     ],
2191                     'ANY: Filter on a single group, user in no groups' => (object) [
2192                         'loginuser' => 'd',
2193                         'groups' => ['groupa'],
2194                         'jointype' => filter::JOINTYPE_ANY,
2195                         'count' => 0,
2196                         'expectedusers' => ['exception'],
2197                     ],
2198                     'ANY: Filter on multiple groups, user in one group (ignore invalid groups)' => (object) [
2199                         'loginuser' => 'a',
2200                         'groups' => ['groupa', 'groupb'],
2201                         'jointype' => filter::JOINTYPE_ANY,
2202                         'count' => 2,
2203                         'expectedusers' => [
2204                             'a',
2205                             'c',
2206                         ],
2207                     ],
2208                     'ANY: Filter on multiple groups, user in multiple groups' => (object) [
2209                         'loginuser' => 'c',
2210                         'groups' => ['groupa', 'groupb'],
2211                         'jointype' => filter::JOINTYPE_ANY,
2212                         'count' => 3,
2213                         'expectedusers' => [
2214                             'a',
2215                             'b',
2216                             'c',
2217                         ],
2218                     ],
2219                     'ANY: Filter on multiple groups or no groups, user in multiple groups (ignore no groups)' => (object) [
2220                         'loginuser' => 'c',
2221                         'groups' => ['groupa', 'groupb', 'nogroups'],
2222                         'jointype' => filter::JOINTYPE_ANY,
2223                         'count' => 3,
2224                         'expectedusers' => [
2225                             'a',
2226                             'b',
2227                             'c',
2228                         ],
2229                     ],
2231                     // Tests for jointype: ALL.
2232                     'ALL: No filter, user in one group' => (object) [
2233                         'loginuser' => 'a',
2234                         'groups' => [],
2235                         'jointype' => filter::JOINTYPE_ALL,
2236                         'count' => 2,
2237                         'expectedusers' => [
2238                             'a',
2239                             'c',
2240                         ],
2241                     ],
2242                     'ALL: No filter, user in multiple groups' => (object) [
2243                         'loginuser' => 'c',
2244                         'groups' => [],
2245                         'jointype' => filter::JOINTYPE_ALL,
2246                         'count' => 3,
2247                         'expectedusers' => [
2248                             'a',
2249                             'b',
2250                             'c',
2251                         ],
2252                     ],
2253                     'ALL: No filter, user in no groups' => (object) [
2254                         'loginuser' => 'd',
2255                         'groups' => [],
2256                         'jointype' => filter::JOINTYPE_ALL,
2257                         'count' => 0,
2258                         'expectedusers' => ['exception'],
2259                     ],
2260                     'ALL: Filter on a single group, user in one group' => (object) [
2261                         'loginuser' => 'a',
2262                         'groups' => ['groupa'],
2263                         'jointype' => filter::JOINTYPE_ALL,
2264                         'count' => 2,
2265                         'expectedusers' => [
2266                             'a',
2267                             'c',
2268                         ],
2269                     ],
2270                     'ALL: Filter on a single group, user in multple groups' => (object) [
2271                         'loginuser' => 'c',
2272                         'groups' => ['groupa'],
2273                         'jointype' => filter::JOINTYPE_ALL,
2274                         'count' => 2,
2275                         'expectedusers' => [
2276                             'a',
2277                             'c',
2278                         ],
2279                     ],
2280                     'ALL: Filter on a single group, user in no groups' => (object) [
2281                         'loginuser' => 'd',
2282                         'groups' => ['groupa'],
2283                         'jointype' => filter::JOINTYPE_ALL,
2284                         'count' => 0,
2285                         'expectedusers' => ['exception'],
2286                     ],
2287                     'ALL: Filter on multiple groups, user in one group (ignore invalid groups)' => (object) [
2288                         'loginuser' => 'a',
2289                         'groups' => ['groupa', 'groupb'],
2290                         'jointype' => filter::JOINTYPE_ALL,
2291                         'count' => 2,
2292                         'expectedusers' => [
2293                             'a',
2294                             'c',
2295                         ],
2296                     ],
2297                     'ALL: Filter on multiple groups, user in multiple groups' => (object) [
2298                         'loginuser' => 'c',
2299                         'groups' => ['groupa', 'groupb'],
2300                         'jointype' => filter::JOINTYPE_ALL,
2301                         'count' => 1,
2302                         'expectedusers' => [
2303                             'c',
2304                         ],
2305                     ],
2306                     'ALL: Filter on multiple groups or no groups, user in multiple groups (ignore no groups)' => (object) [
2307                         'loginuser' => 'c',
2308                         'groups' => ['groupa', 'groupb', 'nogroups'],
2309                         'jointype' => filter::JOINTYPE_ALL,
2310                         'count' => 1,
2311                         'expectedusers' => [
2312                             'c',
2313                         ],
2314                     ],
2316                     // Tests for jointype: NONE.
2317                     'NONE: No filter, user in one group' => (object) [
2318                         'loginuser' => 'a',
2319                         'groups' => [],
2320                         'jointype' => filter::JOINTYPE_NONE,
2321                         'count' => 2,
2322                         'expectedusers' => [
2323                             'a',
2324                             'c',
2325                         ],
2326                     ],
2327                     'NONE: No filter, user in multiple groups' => (object) [
2328                         'loginuser' => 'c',
2329                         'groups' => [],
2330                         'jointype' => filter::JOINTYPE_NONE,
2331                         'count' => 3,
2332                         'expectedusers' => [
2333                             'a',
2334                             'b',
2335                             'c',
2336                         ],
2337                     ],
2338                     'NONE: No filter, user in no groups' => (object) [
2339                         'loginuser' => 'd',
2340                         'groups' => [],
2341                         'jointype' => filter::JOINTYPE_NONE,
2342                         'count' => 0,
2343                         'expectedusers' => ['exception'],
2344                     ],
2345                     'NONE: Filter on a single group, user in one group' => (object) [
2346                         'loginuser' => 'a',
2347                         'groups' => ['groupa'],
2348                         'jointype' => filter::JOINTYPE_NONE,
2349                         'count' => 0,
2350                         'expectedusers' => [],
2351                     ],
2352                     'NONE: Filter on a single group, user in multple groups' => (object) [
2353                         'loginuser' => 'c',
2354                         'groups' => ['groupa'],
2355                         'jointype' => filter::JOINTYPE_NONE,
2356                         'count' => 1,
2357                         'expectedusers' => [
2358                             'b',
2359                         ],
2360                     ],
2361                     'NONE: Filter on a single group, user in no groups' => (object) [
2362                         'loginuser' => 'd',
2363                         'groups' => ['groupa'],
2364                         'jointype' => filter::JOINTYPE_NONE,
2365                         'count' => 0,
2366                         'expectedusers' => ['exception'],
2367                     ],
2368                     'NONE: Filter on multiple groups, user in one group (ignore invalid groups)' => (object) [
2369                         'loginuser' => 'a',
2370                         'groups' => ['groupa', 'groupb'],
2371                         'jointype' => filter::JOINTYPE_NONE,
2372                         'count' => 0,
2373                         'expectedusers' => [],
2374                     ],
2375                     'NONE: Filter on multiple groups, user in multiple groups' => (object) [
2376                         'loginuser' => 'c',
2377                         'groups' => ['groupa', 'groupb'],
2378                         'jointype' => filter::JOINTYPE_NONE,
2379                         'count' => 0,
2380                         'expectedusers' => [],
2381                     ],
2382                     'NONE: Filter on multiple groups or no groups, user in multiple groups (ignore no groups)' => (object) [
2383                         'loginuser' => 'c',
2384                         'groups' => ['groupa', 'groupb', 'nogroups'],
2385                         'jointype' => filter::JOINTYPE_NONE,
2386                         'count' => 0,
2387                         'expectedusers' => [],
2388                     ],
2389                 ],
2390             ],
2391         ];
2393         $finaltests = [];
2394         foreach ($tests as $testname => $testdata) {
2395             foreach ($testdata->expect as $expectname => $expectdata) {
2396                 $finaltests["{$testname} => {$expectname}"] = [
2397                     'users' => $testdata->users,
2398                     'groupsavailable' => $testdata->groupsavailable,
2399                     'filtergroups' => $expectdata->groups,
2400                     'jointype' => $expectdata->jointype,
2401                     'count' => $expectdata->count,
2402                     'expectedusers' => $expectdata->expectedusers,
2403                     'loginusername' => $expectdata->loginuser,
2404                 ];
2405             }
2406         }
2408         return $finaltests;
2409     }
2412     /**
2413      * Ensure that the last access filter works as expected with the provided test cases.
2414      *
2415      * @param array $usersdata The list of users to create
2416      * @param array $accesssince The last access data to filter by
2417      * @param int $jointype The join type to use when combining filter values
2418      * @param int $count The expected count
2419      * @param array $expectedusers
2420      * @dataProvider accesssince_provider
2421      */
2422     public function test_accesssince_filter(array $usersdata, array $accesssince, int $jointype, int $count,
2423             array $expectedusers): void {
2425         $course = $this->getDataGenerator()->create_course();
2426         $coursecontext = context_course::instance($course->id);
2427         $users = [];
2429         foreach ($usersdata as $username => $userdata) {
2430             $usertimestamp = empty($userdata['lastlogin']) ? 0 : strtotime($userdata['lastlogin']);
2432             $user = $this->getDataGenerator()->create_user(['username' => $username]);
2433             $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
2435             // Create the record of the user's last access to the course.
2436             if ($usertimestamp > 0) {
2437                 $this->getDataGenerator()->create_user_course_lastaccess($user, $course, $usertimestamp);
2438             }
2440             $users[$username] = $user;
2441         }
2443         // Create a secondary course with users. We should not see these users.
2444         $this->create_course_with_users(1, 1, 1, 1);
2446         // Create the basic filter.
2447         $filterset = new participants_filterset();
2448         $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id]));
2450         // Create the last access filter.
2451         $lastaccessfilter = new integer_filter('accesssince');
2452         $filterset->add_filter($lastaccessfilter);
2454         // Configure the filter.
2455         foreach ($accesssince as $accessstring) {
2456             $lastaccessfilter->add_filter_value(strtotime($accessstring));
2457         }
2458         $lastaccessfilter->set_join_type($jointype);
2460         // Run the search.
2461         $search = new participants_search($course, $coursecontext, $filterset);
2462         $rs = $search->get_participants();
2463         $this->assertInstanceOf(moodle_recordset::class, $rs);
2464         $records = $this->convert_recordset_to_array($rs);
2466         $this->assertCount($count, $records);
2467         $this->assertEquals($count, $search->get_total_participants_count());
2469         foreach ($expectedusers as $expecteduser) {
2470             $this->assertArrayHasKey($users[$expecteduser]->id, $records);
2471         }
2472     }
2474     /**
2475      * Data provider for last access filter tests.
2476      *
2477      * @return array
2478      */
2479     public function accesssince_provider(): array {
2480         $tests = [
2481             // Users with different last access times.
2482             'Users in different groups' => (object) [
2483                 'users' => [
2484                     'a' => [
2485                         'lastlogin' => '-3 days',
2486                     ],
2487                     'b' => [
2488                         'lastlogin' => '-2 weeks',
2489                     ],
2490                     'c' => [
2491                         'lastlogin' => '-5 months',
2492                     ],
2493                     'd' => [
2494                         'lastlogin' => '-11 months',
2495                     ],
2496                     'e' => [
2497                         // Never logged in.
2498                         'lastlogin' => '',
2499                     ],
2500                 ],
2501                 'expect' => [
2502                     // Tests for jointype: ANY.
2503                     'ANY: No filter' => (object) [
2504                         'accesssince' => [],
2505                         'jointype' => filter::JOINTYPE_ANY,
2506                         'count' => 5,
2507                         'expectedusers' => [
2508                             'a',
2509                             'b',
2510                             'c',
2511                             'd',
2512                             'e',
2513                         ],
2514                     ],
2515                     'ANY: Filter on last login more than 1 year ago' => (object) [
2516                         'accesssince' => ['-1 year'],
2517                         'jointype' => filter::JOINTYPE_ANY,
2518                         'count' => 1,
2519                         'expectedusers' => [
2520                             'e',
2521                         ],
2522                     ],
2523                     'ANY: Filter on last login more than 6 months ago' => (object) [
2524                         'accesssince' => ['-6 months'],
2525                         'jointype' => filter::JOINTYPE_ANY,
2526                         'count' => 2,
2527                         'expectedusers' => [
2528                             'd',
2529                             'e',
2530                         ],
2531                     ],
2532                     'ANY: Filter on last login more than 3 weeks ago' => (object) [
2533                         'accesssince' => ['-3 weeks'],
2534                         'jointype' => filter::JOINTYPE_ANY,
2535                         'count' => 3,
2536                         'expectedusers' => [
2537                             'c',
2538                             'd',
2539                             'e',
2540                         ],
2541                     ],
2542                     'ANY: Filter on last login more than 5 days ago' => (object) [
2543                         'accesssince' => ['-5 days'],
2544                         'jointype' => filter::JOINTYPE_ANY,
2545                         'count' => 4,
2546                         'expectedusers' => [
2547                             'b',
2548                             'c',
2549                             'd',
2550                             'e',
2551                         ],
2552                     ],
2553                     'ANY: Filter on last login more than 2 days ago' => (object) [
2554                         'accesssince' => ['-2 days'],
2555                         'jointype' => filter::JOINTYPE_ANY,
2556                         'count' => 5,
2557                         'expectedusers' => [
2558                             'a',
2559                             'b',
2560                             'c',
2561                             'd',
2562                             'e',
2563                         ],
2564                     ],
2566                     // Tests for jointype: ALL.
2567                     'ALL: No filter' => (object) [
2568                         'accesssince' => [],
2569                         'jointype' => filter::JOINTYPE_ALL,
2570                         'count' => 5,
2571                         'expectedusers' => [
2572                             'a',
2573                             'b',
2574                             'c',
2575                             'd',
2576                             'e',
2577                         ],
2578                     ],
2579                     'ALL: Filter on last login more than 1 year ago' => (object) [
2580                         'accesssince' => ['-1 year'],
2581                         'jointype' => filter::JOINTYPE_ALL,
2582                         'count' => 1,
2583                         'expectedusers' => [
2584                             'e',
2585                         ],
2586                     ],
2587                     'ALL: Filter on last login more than 6 months ago' => (object) [
2588                         'accesssince' => ['-6 months'],
2589                         'jointype' => filter::JOINTYPE_ALL,
2590                         'count' => 2,
2591                         'expectedusers' => [
2592                             'd',
2593                             'e',
2594                         ],
2595                     ],
2596                     'ALL: Filter on last login more than 3 weeks ago' => (object) [
2597                         'accesssince' => ['-3 weeks'],
2598                         'jointype' => filter::JOINTYPE_ALL,
2599                         'count' => 3,
2600                         'expectedusers' => [
2601                             'c',
2602                             'd',
2603                             'e',
2604                         ],
2605                     ],
2606                     'ALL: Filter on last login more than 5 days ago' => (object) [
2607                         'accesssince' => ['-5 days'],
2608                         'jointype' => filter::JOINTYPE_ALL,
2609                         'count' => 4,
2610                         'expectedusers' => [
2611                             'b',
2612                             'c',
2613                             'd',
2614                             'e',
2615                         ],
2616                     ],
2617                     'ALL: Filter on last login more than 2 days ago' => (object) [
2618                         'accesssince' => ['-2 days'],
2619                         'jointype' => filter::JOINTYPE_ALL,
2620                         'count' => 5,
2621                         'expectedusers' => [
2622                             'a',
2623                             'b',
2624                             'c',
2625                             'd',
2626                             'e',
2627                         ],
2628                     ],
2630                     // Tests for jointype: NONE.
2631                     'NONE: No filter' => (object) [
2632                         'accesssince' => [],
2633                         'jointype' => filter::JOINTYPE_NONE,
2634                         'count' => 5,
2635                         'expectedusers' => [
2636                             'a',
2637                             'b',
2638                             'c',
2639                             'd',
2640                             'e',
2641                         ],
2642                     ],
2643                     'NONE: Filter on last login more than 1 year ago' => (object) [
2644                         'accesssince' => ['-1 year'],
2645                         'jointype' => filter::JOINTYPE_NONE,
2646                         'count' => 4,
2647                         'expectedusers' => [
2648                             'a',
2649                             'b',
2650                             'c',
2651                             'd',
2652                         ],
2653                     ],
2654                     'NONE: Filter on last login more than 6 months ago' => (object) [
2655                         'accesssince' => ['-6 months'],
2656                         'jointype' => filter::JOINTYPE_NONE,
2657                         'count' => 3,
2658                         'expectedusers' => [
2659                             'a',
2660                             'b',
2661                             'c',
2662                         ],
2663                     ],
2664                     'NONE: Filter on last login more than 3 weeks ago' => (object) [
2665                         'accesssince' => ['-3 weeks'],
2666                         'jointype' => filter::JOINTYPE_NONE,
2667                         'count' => 2,
2668                         'expectedusers' => [
2669                             'a',
2670                             'b',
2671                         ],
2672                     ],
2673                     'NONE: Filter on last login more than 5 days ago' => (object) [
2674                         'accesssince' => ['-5 days'],
2675                         'jointype' => filter::JOINTYPE_NONE,
2676                         'count' => 1,
2677                         'expectedusers' => [
2678                             'a',
2679                         ],
2680                     ],
2681                     'NONE: Filter on last login more than 2 days ago' => (object) [
2682                         'accesssince' => ['-2 days'],
2683                         'jointype' => filter::JOINTYPE_NONE,
2684                         'count' => 0,
2685                         'expectedusers' => [],
2686                     ],
2687                 ],
2688             ],
2689         ];
2691         $finaltests = [];
2692         foreach ($tests as $testname => $testdata) {
2693             foreach ($testdata->expect as $expectname => $expectdata) {
2694                 $finaltests["{$testname} => {$expectname}"] = [
2695                     'users' => $testdata->users,
2696                     'accesssince' => $expectdata->accesssince,
2697                     'jointype' => $expectdata->jointype,
2698                     'count' => $expectdata->count,
2699                     'expectedusers' => $expectdata->expectedusers,
2700                 ];
2701             }
2702         }
2704         return $finaltests;
2705     }
2707     /**
2708      * Ensure that the joins between filters in the filterset work as expected with the provided test cases.
2709      *
2710      * @param array $usersdata The list of users to create
2711      * @param array $filterdata The data to filter by
2712      * @param array $groupsavailable The names of groups that should be created in the course
2713      * @param int $jointype The join type to used between each filter being applied
2714      * @param int $count The expected count
2715      * @param array $expectedusers
2716      * @dataProvider filterset_joins_provider
2717      */
2718     public function test_filterset_joins(array $usersdata, array $filterdata, array $groupsavailable, int $jointype, int $count,
2719             array $expectedusers): void {
2720         global $DB;
2722         // Ensure sufficient capabilities to view all statuses.
2723         $this->setAdminUser();
2725         // Remove the default role.
2726         set_config('roleid', 0, 'enrol_manual');
2728         $course = $this->getDataGenerator()->create_course();
2729         $coursecontext = context_course::instance($course->id);
2730         $roles = $DB->get_records_menu('role', [], '', 'shortname, id');
2731         $users = [];
2733         // Ensure all enrolment methods are enabled (and mapped where required for filtering later).
2734         $enrolinstances = enrol_get_instances($course->id, false);
2735         $enrolinstancesmap = [];
2736         foreach ($enrolinstances as $instance) {
2737             $plugin = enrol_get_plugin($instance->enrol);
2738             $plugin->update_status($instance, ENROL_INSTANCE_ENABLED);
2740             $enrolinstancesmap[$instance->enrol] = (int) $instance->id;
2741         }
2743         // Create the required course groups and mapping.
2744         $nogroupsdata = (object) [
2745             'id' => USERSWITHOUTGROUP,
2746         ];
2748          $groupsdata = ['nogroups' => $nogroupsdata];
2749         foreach ($groupsavailable as $groupname) {
2750             $groupinfo = [
2751                 'courseid' => $course->id,
2752                 'name' => $groupname,
2753             ];
2755             $groupsdata[$groupname] = $this->getDataGenerator()->create_group($groupinfo);
2756         }
2758         // Create test users.
2759         foreach ($usersdata as $username => $userdata) {
2760             $usertimestamp = empty($userdata['lastlogin']) ? 0 : strtotime($userdata['lastlogin']);
2761             unset($userdata['lastlogin']);
2763             // Prevent randomly generated field values that may cause false fails.
2764             $userdata['firstnamephonetic'] = $userdata['firstnamephonetic'] ?? $userdata['firstname'];
2765             $userdata['lastnamephonetic'] = $userdata['lastnamephonetic'] ?? $userdata['lastname'];
2766             $userdata['middlename'] = $userdata['middlename'] ?? '';
2767             $userdata['alternatename'] = $userdata['alternatename'] ?? $username;
2769             $user = $this->getDataGenerator()->create_user($userdata);
2771             foreach ($userdata['enrolments'] as $details) {
2772                 $this->getDataGenerator()->enrol_user($user->id, $course->id, $roles[$details['role']],
2773                         $details['method'], 0, 0, $details['status']);
2774             }
2776             foreach ($userdata['groups'] as $groupname) {
2777                 $userinfo = [
2778                     'userid' => $user->id,
2779                     'groupid' => (int) $groupsdata[$groupname]->id,
2780                 ];
2781                 $this->getDataGenerator()->create_group_member($userinfo);
2782             }
2784             if ($usertimestamp > 0) {
2785                 $this->getDataGenerator()->create_user_course_lastaccess($user, $course, $usertimestamp);
2786             }
2788             $users[$username] = $user;
2789         }
2791         // Create a secondary course with users. We should not see these users.
2792         $this->create_course_with_users(10, 10, 10, 10);
2794         // Create the basic filterset.
2795         $filterset = new participants_filterset();
2796         $filterset->set_join_type($jointype);
2797         $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id]));
2799         // Apply the keywords filter if required.
2800         if (array_key_exists('keywords', $filterdata)) {
2801             $keywordfilter = new string_filter('keywords');
2802             $filterset->add_filter($keywordfilter);
2804             foreach ($filterdata['keywords']['values'] as $keyword) {
2805                 $keywordfilter->add_filter_value($keyword);
2806             }
2807             $keywordfilter->set_join_type($filterdata['keywords']['jointype']);
2808         }
2810         // Apply enrolment methods filter if required.
2811         if (array_key_exists('enrolmethods', $filterdata)) {
2812             $enrolmethodfilter = new integer_filter('enrolments');
2813             $filterset->add_filter($enrolmethodfilter);
2815             foreach ($filterdata['enrolmethods']['values'] as $enrolmethod) {
2816                 $enrolmethodfilter->add_filter_value($enrolinstancesmap[$enrolmethod]);
2817             }
2818             $enrolmethodfilter->set_join_type($filterdata['enrolmethods']['jointype']);
2819         }
2821         // Apply roles filter if required.
2822         if (array_key_exists('courseroles', $filterdata)) {
2823             $rolefilter = new integer_filter('roles');
2824             $filterset->add_filter($rolefilter);
2826             foreach ($filterdata['courseroles']['values'] as $rolename) {
2827                 $rolefilter->add_filter_value((int) $roles[$rolename]);
2828             }
2829             $rolefilter->set_join_type($filterdata['courseroles']['jointype']);
2830         }
2832         // Apply status filter if required.
2833         if (array_key_exists('status', $filterdata)) {
2834             $statusfilter = new integer_filter('status');
2835             $filterset->add_filter($statusfilter);
2837             foreach ($filterdata['status']['values'] as $status) {
2838                 $statusfilter->add_filter_value($status);
2839             }
2840             $statusfilter->set_join_type($filterdata['status']['jointype']);
2841         }
2843         // Apply groups filter if required.
2844         if (array_key_exists('groups', $filterdata)) {
2845             $groupsfilter = new integer_filter('groups');
2846             $filterset->add_filter($groupsfilter);
2848             foreach ($filterdata['groups']['values'] as $filtergroupname) {
2849                 $groupsfilter->add_filter_value((int) $groupsdata[$filtergroupname]->id);
2850             }
2851             $groupsfilter->set_join_type($filterdata['groups']['jointype']);
2852         }
2854         // Apply last access filter if required.
2855         if (array_key_exists('accesssince', $filterdata)) {
2856             $lastaccessfilter = new integer_filter('accesssince');
2857             $filterset->add_filter($lastaccessfilter);
2859             foreach ($filterdata['accesssince']['values'] as $accessstring) {
2860                 $lastaccessfilter->add_filter_value(strtotime($accessstring));
2861             }
2862             $lastaccessfilter->set_join_type($filterdata['accesssince']['jointype']);
2863         }
2865         // Run the search.
2866         $search = new participants_search($course, $coursecontext, $filterset);
2867         $rs = $search->get_participants();
2868         $this->assertInstanceOf(moodle_recordset::class, $rs);
2869         $records = $this->convert_recordset_to_array($rs);
2871         $this->assertCount($count, $records);
2872         $this->assertEquals($count, $search->get_total_participants_count());
2874         foreach ($expectedusers as $expecteduser) {
2875             $this->assertArrayHasKey($users[$expecteduser]->id, $records);
2876         }
2877     }
2879     /**
2880      * Data provider for filterset join tests.
2881      *
2882      * @return array
2883      */
2884     public function filterset_joins_provider(): array {
2885         $tests = [
2886             // Users with different configurations.
2887             'Users with different configurations' => (object) [
2888                 'groupsavailable' => [
2889                     'groupa',
2890                     'groupb',
2891                     'groupc',
2892                 ],
2893                 'users' => [
2894                     'adam.ant' => [
2895                         'firstname' => 'Adam',
2896                         'lastname' => 'Ant',
2897                         'enrolments' => [
2898                             [
2899                                 'role' => 'student',
2900                                 'method' => 'manual',
2901                                 'status' => ENROL_USER_ACTIVE,
2902                             ],
2903                         ],
2904                         'groups' => ['groupa'],
2905                         'lastlogin' => '-3 days',
2906                     ],
2907                     'barbara.bennett' => [
2908                         'firstname' => 'Barbara',
2909                         'lastname' => 'Bennett',
2910                         'enrolments' => [
2911                             [
2912                                 'role' => 'student',
2913                                 'method' => 'manual',
2914                                 'status' => ENROL_USER_ACTIVE,
2915                             ],
2916                             [
2917                                 'role' => 'teacher',
2918                                 'method' => 'manual',
2919                                 'status' => ENROL_USER_ACTIVE,
2920                             ],
2921                         ],
2922                         'groups' => ['groupb'],
2923                         'lastlogin' => '-2 weeks',
2924                     ],
2925                     'colin.carnforth' => [
2926                         'firstname' => 'Colin',
2927                         'lastname' => 'Carnforth',
2928                         'enrolments' => [
2929                             [
2930                                 'role' => 'editingteacher',
2931                                 'method' => 'self',
2932                                 'status' => ENROL_USER_SUSPENDED,
2933                             ],
2934                         ],
2935                         'groups' => ['groupa', 'groupb'],
2936                         'lastlogin' => '-5 months',
2937                     ],
2938                     'tony.rogers' => [
2939                         'firstname' => 'Anthony',
2940                         'lastname' => 'Rogers',
2941                         'enrolments' => [
2942                             [
2943                                 'role' => 'editingteacher',
2944                                 'method' => 'self',
2945                                 'status' => ENROL_USER_SUSPENDED,
2946                             ],
2947                         ],
2948                         'groups' => [],
2949                         'lastlogin' => '-10 months',
2950                     ],
2951                     'sarah.rester' => [
2952                         'firstname' => 'Sarah',
2953                         'lastname' => 'Rester',
2954                         'email' => 'zazu@example.com',
2955                         'enrolments' => [
2956                             [
2957                                 'role' => 'teacher',
2958                                 'method' => 'manual',
2959                                 'status' => ENROL_USER_ACTIVE,
2960                             ],
2961                             [
2962                                 'role' => 'editingteacher',
2963                                 'method' => 'self',
2964                                 'status' => ENROL_USER_SUSPENDED,
2965                             ],
2966                         ],
2967                         'groups' => [],
2968                         'lastlogin' => '-11 months',
2969                     ],
2970                     'morgan.crikeyson' => [
2971                         'firstname' => 'Morgan',
2972                         'lastname' => 'Crikeyson',
2973                         'enrolments' => [
2974                             [
2975                                 'role' => 'teacher',
2976                                 'method' => 'manual',
2977                                 'status' => ENROL_USER_ACTIVE,
2978                             ],
2979                         ],
2980                         'groups' => ['groupa'],
2981                         'lastlogin' => '-1 week',
2982                     ],
2983                     'jonathan.bravo' => [
2984                         'firstname' => 'Jonathan',
2985                         'lastname' => 'Bravo',
2986                         'enrolments' => [
2987                             [
2988                                 'role' => 'student',
2989                                 'method' => 'manual',
2990                                 'status' => ENROL_USER_ACTIVE,
2991                             ],
2992                         ],
2993                         'groups' => [],
2994                         // Never logged in.
2995                         'lastlogin' => '',
2996                     ],
2997                 ],
2998                 'expect' => [
2999                     // Tests for jointype: ANY.
3000                     'ANY: No filters in filterset' => (object) [
3001                         'filterdata' => [],
3002                         'jointype' => filter::JOINTYPE_ANY,
3003                         'count' => 7,
3004                         'expectedusers' => [
3005                             'adam.ant',
3006                             'barbara.bennett',
3007                             'colin.carnforth',
3008                             'tony.rogers',
3009                             'sarah.rester',
3010                             'morgan.crikeyson',
3011                             'jonathan.bravo',
3012                         ],
3013                     ],
3014                     'ANY: Filterset containing a single filter type' => (object) [
3015                         'filterdata' => [
3016                             'enrolmethods' => [
3017                                 'values' => ['self'],
3018                                 'jointype' => filter::JOINTYPE_ANY,
3019                             ],
3020                         ],
3021                         'jointype' => filter::JOINTYPE_ANY,
3022                         'count' => 3,
3023                         'expectedusers' => [
3024                             'colin.carnforth',
3025                             'tony.rogers',
3026                             'sarah.rester',
3027                         ],
3028                     ],
3029                     'ANY: Filterset matching all filter types on different users' => (object) [
3030                         'filterdata' => [
3031                             // Match Adam only.
3032                             'keywords' => [
3033                                 'values' => ['adam'],
3034                                 'jointype' => filter::JOINTYPE_ALL,
3035                             ],
3036                             // Match Sarah only.
3037                             'enrolmethods' => [
3038                                 'values' => ['manual', 'self'],
3039                                 'jointype' => filter::JOINTYPE_ALL,
3040                             ],
3041                             // Match Barbara only.
3042                             'courseroles' => [
3043                                 'values' => ['student', 'teacher'],
3044                                 'jointype' => filter::JOINTYPE_ALL,
3045                             ],
3046                             // Match Sarah only.
3047                             'statuses' => [
3048                                 'values' => ['active', 'suspended'],
3049                                 'jointype' => filter::JOINTYPE_ALL,
3050                             ],
3051                             // Match Colin only.
3052                             'groups' => [
3053                                 'values' => ['groupa', 'groupb'],
3054                                 'jointype' => filter::JOINTYPE_ALL,
3055                             ],
3056                             // Match Jonathan only.
3057                             'accesssince' => [
3058                                 'values' => ['-1 year'],
3059                                 'jointype' => filter::JOINTYPE_ALL,
3060                                 ],
3061                         ],
3062                         'jointype' => filter::JOINTYPE_ANY,
3063                         'count' => 5,
3064                         // Morgan and Tony are not matched, to confirm filtering is not just returning all users.
3065                         'expectedusers' => [
3066                             'adam.ant',
3067                             'barbara.bennett',
3068                             'colin.carnforth',
3069                             'sarah.rester',
3070                             'jonathan.bravo',
3071                         ],
3072                     ],
3074                     // Tests for jointype: ALL.
3075                     'ALL: No filters in filterset' => (object) [
3076                         'filterdata' => [],
3077                         'jointype' => filter::JOINTYPE_ALL,
3078                         'count' => 7,
3079                         'expectedusers' => [
3080                             'adam.ant',
3081                             'barbara.bennett',
3082                             'coli