Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.
<?php
// This file is part of Moodle - https://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.

/**
 * Provides {@link core_user_table_participants_search_test} class.
 *
 * @package   core_user
 * @category  test
 * @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

declare(strict_types=1);

namespace core_user\table;

use advanced_testcase;
use context_course;
use context_coursecat;
use core_table\local\filter\filter;
use core_table\local\filter\integer_filter;
use core_table\local\filter\string_filter;
use core_user\table\participants_filterset;
use core_user\table\participants_search;
use moodle_recordset;
use stdClass;

/**
 * Tests for the implementation of {@link core_user_table_participants_search} class.
 *
 * @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class participants_search_test extends advanced_testcase {

    /**
     * Helper to convert a moodle_recordset to an array of records.
     *
     * @param moodle_recordset $recordset
     * @return array
     */
    protected function convert_recordset_to_array(moodle_recordset $recordset): array {
        $records = [];
        foreach ($recordset as $record) {
            $records[$record->id] = $record;
        }
        $recordset->close();

        return $records;
    }

    /**
     * Create and enrol a set of users into the specified course.
     *
     * @param stdClass $course
     * @param int $count
     * @param null|string $role
     * @return array
     */
    protected function create_and_enrol_users(stdClass $course, int $count, ?string $role = null): array {
        $this->resetAfterTest(true);
        $users = [];

        for ($i = 0; $i < $count; $i++) {
            $user = $this->getDataGenerator()->create_user();
            $this->getDataGenerator()->enrol_user($user->id, $course->id, $role);
            $users[] = $user;
        }

        return $users;
    }

    /**
     * Create a new course with several types of user.
     *
     * @param int $editingteachers The number of editing teachers to create in the course.
     * @param int $teachers The number of non-editing teachers to create in the course.
     * @param int $students The number of students to create in the course.
     * @param int $norole The number of users with no role to create in the course.
     * @return stdClass
     */
    protected function create_course_with_users(int $editingteachers, int $teachers, int $students, int $norole): stdClass {
        $data = (object) [
            'course' => $this->getDataGenerator()->create_course(),
            'editingteachers' => [],
            'teachers' => [],
            'students' => [],
            'norole' => [],
        ];

        $data->context = context_course::instance($data->course->id);

        $data->editingteachers = $this->create_and_enrol_users($data->course, $editingteachers, 'editingteacher');
        $data->teachers = $this->create_and_enrol_users($data->course, $teachers, 'teacher');
        $data->students = $this->create_and_enrol_users($data->course, $students, 'student');
        $data->norole = $this->create_and_enrol_users($data->course, $norole);

        return $data;
    }
    /**
     * Ensure that the roles filter works as expected with the provided test cases.
     *
     * @param array $usersdata The list of users and their roles to create
     * @param array $testroles The list of roles to filter by
     * @param int $jointype The join type to use when combining filter values
     * @param int $count The expected count
     * @param array $expectedusers
     * @dataProvider role_provider
     */
    public function test_roles_filter(array $usersdata, array $testroles, int $jointype, int $count, array $expectedusers): void {
        global $DB;

        $roles = $DB->get_records_menu('role', [], '', 'shortname, id');

        // Remove the default role.
        set_config('roleid', 0, 'enrol_manual');

        $course = $this->getDataGenerator()->create_course();
        $coursecontext = context_course::instance($course->id);

        $category = $DB->get_record('course_categories', ['id' => $course->category]);
        $categorycontext = context_coursecat::instance($category->id);

        $users = [];

        foreach ($usersdata as $username => $userdata) {
            $user = $this->getDataGenerator()->create_user(['username' => $username]);

            if (array_key_exists('courseroles', $userdata)) {
                $this->getDataGenerator()->enrol_user($user->id, $course->id, null);
                foreach ($userdata['courseroles'] as $rolename) {
                    $this->getDataGenerator()->role_assign($roles[$rolename], $user->id, $coursecontext->id);
                }
            }

            if (array_key_exists('categoryroles', $userdata)) {
                foreach ($userdata['categoryroles'] as $rolename) {
                    $this->getDataGenerator()->role_assign($roles[$rolename], $user->id, $categorycontext->id);
                }
            }
            $users[$username] = $user;
        }

        // Create a secondary course with users. We should not see these users.
        $this->create_course_with_users(1, 1, 1, 1);

        // Create the basic filter.
        $filterset = new participants_filterset();
        $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id]));

        // Create the role filter.
        $rolefilter = new integer_filter('roles');
        $filterset->add_filter($rolefilter);

        // Configure the filter.
        foreach ($testroles as $rolename) {
            $rolefilter->add_filter_value((int) $roles[$rolename]);
        }
        $rolefilter->set_join_type($jointype);

        // Run the search.
        $search = new participants_search($course, $coursecontext, $filterset);
        $rs = $search->get_participants();
        $this->assertInstanceOf(moodle_recordset::class, $rs);
        $records = $this->convert_recordset_to_array($rs);

        $this->assertCount($count, $records);
        $this->assertEquals($count, $search->get_total_participants_count());

        foreach ($expectedusers as $expecteduser) {
            $this->assertArrayHasKey($users[$expecteduser]->id, $records);
        }
    }

    /**
     * Data provider for role tests.
     *
     * @return array
     */
    public function role_provider(): array {
        $tests = [
            // Users who only have one role each.
            'Users in each role' => (object) [
                'users' => [
                    'a' => [
                        'courseroles' => [
                            'student',
                        ],
                    ],
                    'b' => [
                        'courseroles' => [
                            'student',
                        ],
                    ],
                    'c' => [
                        'courseroles' => [
                            'editingteacher',
                        ],
                    ],
                    'd' => [
                        'courseroles' => [
                            'editingteacher',
                        ],
                    ],
                    'e' => [
                        'courseroles' => [
                            'teacher',
                        ],
                    ],
                    'f' => [
                        'courseroles' => [
                            'teacher',
                        ],
                    ],
                    // User is enrolled in the course without role.
                    'g' => [
                        'courseroles' => [
                        ],
                    ],

                    // User is a category manager and also enrolled without role in the course.
                    'h' => [
                        'courseroles' => [
                        ],
                        'categoryroles' => [
                            'manager',
                        ],
                    ],

                    // User is a category manager and not enrolled in the course.
                    // This user should not show up in any filter.
                    'i' => [
                        'categoryroles' => [
                            'manager',
                        ],
                    ],
                ],
                'expect' => [
                    // Tests for jointype: ANY.
                    'ANY: No role filter' => (object) [
                        'roles' => [],
                        'jointype' => filter::JOINTYPE_ANY,
                        'count' => 8,
                        'expectedusers' => [
                            'a',
                            'b',
                            'c',
                            'd',
                            'e',
                            'f',
                            'g',
                            'h',
                        ],
                    ],
                    'ANY: Filter on student' => (object) [
                        'roles' => ['student'],
                        'jointype' => filter::JOINTYPE_ANY,
                        'count' => 2,
                        'expectedusers' => [
                            'a',
                            'b',
                        ],
                    ],
                    'ANY: Filter on student, teacher' => (object) [
                        'roles' => ['student', 'teacher'],
                        'jointype' => filter::JOINTYPE_ANY,
                        'count' => 4,
                        'expectedusers' => [
                            'a',
                            'b',
                            'e',
                            'f',
                        ],
                    ],
                    'ANY: Filter on student, manager (category level role)' => (object) [
                        'roles' => ['student', 'manager'],
                        'jointype' => filter::JOINTYPE_ANY,
                        'count' => 3,
                        'expectedusers' => [
                            'a',
                            'b',
                            'h',
                        ],
                    ],
                    'ANY: Filter on student, coursecreator (not assigned)' => (object) [
                        'roles' => ['student', 'coursecreator'],
                        'jointype' => filter::JOINTYPE_ANY,
                        'count' => 2,
                        'expectedusers' => [
                            'a',
                            'b',
                        ],
                    ],

                    // Tests for jointype: ALL.
                    'ALL: No role filter' => (object) [
                        'roles' => [],
                        'jointype' => filter::JOINTYPE_ALL,
                        'count' => 8,
                        'expectedusers' => [
                            'a',
                            'b',
                            'c',
                            'd',
                            'e',
                            'f',
                            'g',
                            'h',
                        ],
                    ],
                    'ALL: Filter on student' => (object) [
                        'roles' => ['student'],
                        'jointype' => filter::JOINTYPE_ALL,
                        'count' => 2,
                        'expectedusers' => [
                            'a',
                            'b',
                        ],
                    ],
                    'ALL: Filter on student, teacher' => (object) [
                        'roles' => ['student', 'teacher'],
                        'jointype' => filter::JOINTYPE_ALL,
                        'count' => 0,
                        'expectedusers' => [],
                    ],
                    'ALL: Filter on student, manager (category level role))' => (object) [
                        'roles' => ['student', 'manager'],
                        'jointype' => filter::JOINTYPE_ALL,
                        'count' => 0,
                        'expectedusers' => [],
                    ],
                    'ALL: Filter on student, coursecreator (not assigned))' => (object) [
                        'roles' => ['student', 'coursecreator'],
                        'jointype' => filter::JOINTYPE_ALL,
                        'count' => 0,
                        'expectedusers' => [],
                    ],

                    // Tests for jointype: NONE.
                    'NONE: No role filter' => (object) [
                        'roles' => [],
                        'jointype' => filter::JOINTYPE_NONE,
                        'count' => 8,
                        'expectedusers' => [
                            'a',
                            'b',
                            'c',
                            'd',
                            'e',
                            'f',
                            'g',
                            'h',
                        ],
                    ],
                    'NONE: Filter on student' => (object) [
                        'roles' => ['student'],
                        'jointype' => filter::JOINTYPE_NONE,
                        'count' => 6,
                        'expectedusers' => [
                            'c',
                            'd',
                            'e',
                            'f',
                            'g',
                            'h',
                        ],
                    ],
                    'NONE: Filter on student, teacher' => (object) [
                        'roles' => ['student', 'teacher'],
                        'jointype' => filter::JOINTYPE_NONE,
                        'count' => 4,
                        'expectedusers' => [
                            'c',
                            'd',
                            'g',
                            'h',
                        ],
                    ],
                    'NONE: Filter on student, manager (category level role))' => (object) [
                        'roles' => ['student', 'manager'],
                        'jointype' => filter::JOINTYPE_NONE,
                        'count' => 5,
                        'expectedusers' => [
                            'c',
                            'd',
                            'e',
                            'f',
                            'g',
                        ],
                    ],
                    'NONE: Filter on student, coursecreator (not assigned))' => (object) [
                        'roles' => ['student', 'coursecreator'],
                        'jointype' => filter::JOINTYPE_NONE,
                        'count' => 6,
                        'expectedusers' => [
                            'c',
                            'd',
                            'e',
                            'f',
                            'g',
                            'h',
                        ],
                    ],
                ],
            ],
            'Users with multiple roles' => (object) [
                'users' => [
                    'a' => [
                        'courseroles' => [
                            'student',
                        ],
                    ],
                    'b' => [
                        'courseroles' => [
                            'student',
                            'teacher',
                        ],
                    ],
                    'c' => [
                        'courseroles' => [
                            'editingteacher',
                        ],
                    ],
                    'd' => [
                        'courseroles' => [
                            'editingteacher',
                        ],
                    ],
                    'e' => [
                        'courseroles' => [
                            'teacher',
                            'editingteacher',
                        ],
                    ],
                    'f' => [
                        'courseroles' => [
                            'teacher',
                        ],
                    ],

                    // User is enrolled in the course without role.
                    'g' => [
                        'courseroles' => [
                        ],
                    ],

                    // User is a category manager and also enrolled without role in the course.
                    'h' => [
                        'courseroles' => [
                        ],
                        'categoryroles' => [
                            'manager',
                        ],
                    ],

                    // User is a category manager and not enrolled in the course.
                    // This user should not show up in any filter.
                    'i' => [
                        'categoryroles' => [
                            'manager',
                        ],
                    ],
                ],
                'expect' => [
                    // Tests for jointype: ANY.
                    'ANY: No role filter' => (object) [
                        'roles' => [],
                        'jointype' => filter::JOINTYPE_ANY,
                        'count' => 8,
                        'expectedusers' => [
                            'a',
                            'b',
                            'c',
                            'd',
                            'e',
                            'f',
                            'g',
                            'h',
                        ],
                    ],
                    'ANY: Filter on student' => (object) [
                        'roles' => ['student'],
                        'jointype' => filter::JOINTYPE_ANY,
                        'count' => 2,
                        'expectedusers' => [
                            'a',
                            'b',
                        ],
                    ],
                    'ANY: Filter on teacher' => (object) [
                        'roles' => ['teacher'],
                        'jointype' => filter::JOINTYPE_ANY,
                        'count' => 3,
                        'expectedusers' => [
                            'b',
                            'e',
                            'f',
                        ],
                    ],
                    'ANY: Filter on editingteacher' => (object) [
                        'roles' => ['editingteacher'],
                        'jointype' => filter::JOINTYPE_ANY,
                        'count' => 3,
                        'expectedusers' => [
                            'c',
                            'd',
                            'e',
                        ],
                    ],
                    'ANY: Filter on student, teacher' => (object) [
                        'roles' => ['student', 'teacher'],
                        'jointype' => filter::JOINTYPE_ANY,
                        'count' => 4,
                        'expectedusers' => [
                            'a',
                            'b',
                            'e',
                            'f',
                        ],
                    ],
                    'ANY: Filter on teacher, editingteacher' => (object) [
                        'roles' => ['teacher', 'editingteacher'],
                        'jointype' => filter::JOINTYPE_ANY,
                        'count' => 5,
                        'expectedusers' => [
                            'b',
                            'c',
                            'd',
                            'e',
                            'f',
                        ],
                    ],
                    'ANY: Filter on student, manager (category level role)' => (object) [
                        'roles' => ['student', 'manager'],
                        'jointype' => filter::JOINTYPE_ANY,
                        'count' => 3,
                        'expectedusers' => [
                            'a',
                            'b',
                            'h',
                        ],
                    ],
                    'ANY: Filter on student, coursecreator (not assigned)' => (object) [
                        'roles' => ['student', 'coursecreator'],
                        'jointype' => filter::JOINTYPE_ANY,
                        'count' => 2,
                        'expectedusers' => [
                            'a',
                            'b',
                        ],
                    ],

                    // Tests for jointype: ALL.
                    'ALL: No role filter' => (object) [
                        'roles' => [],
                        'jointype' => filter::JOINTYPE_ALL,
                        'count' => 8,
                        'expectedusers' => [
                            'a',
                            'b',
                            'c',
                            'd',
                            'e',
                            'f',
                            'g',
                            'h',
                        ],
                    ],
                    'ALL: Filter on student' => (object) [
                        'roles' => ['student'],
                        'jointype' => filter::JOINTYPE_ALL,
                        'count' => 2,
                        'expectedusers' => [
                            'a',
                            'b',
                        ],
                    ],
                    'ALL: Filter on teacher' => (object) [
                        'roles' => ['teacher'],
                        'jointype' => filter::JOINTYPE_ALL,
                        'count' => 3,
                        'expectedusers' => [
                            'b',
                            'e',
                            'f',
                        ],
                    ],
                    'ALL: Filter on editingteacher' => (object) [
                        'roles' => ['editingteacher'],
                        'jointype' => filter::JOINTYPE_ALL,
                        'count' => 3,
                        'expectedusers' => [
                            'c',
                            'd',
                            'e',
                        ],
                    ],
                    'ALL: Filter on student, teacher' => (object) [
                        'roles' => ['student', 'teacher'],
                        'jointype' => filter::JOINTYPE_ALL,
                        'count' => 1,
                        'expectedusers' => [
                            'b',
                        ],
                    ],
                    'ALL: Filter on teacher, editingteacher' => (object) [
                        'roles' => ['teacher', 'editingteacher'],
                        'jointype' => filter::JOINTYPE_ALL,
                        'count' => 1,
                        'expectedusers' => [
                            'e',
                        ],
                    ],
                    'ALL: Filter on student, manager (category level role)' => (object) [
                        'roles' => ['student', 'manager'],
                        'jointype' => filter::JOINTYPE_ALL,
                        'count' => 0,
                        'expectedusers' => [],
                    ],
                    'ALL: Filter on student, coursecreator (not assigned)' => (object) [
                        'roles' => ['student', 'coursecreator'],
                        'jointype' => filter::JOINTYPE_ALL,
                        'count' => 0,
                        'expectedusers' => [],
                    ],

                    // Tests for jointype: NONE.
                    'NONE: No role filter' => (object) [
                        'roles' => [],
                        'jointype' => filter::JOINTYPE_NONE,
                        'count' => 8,
                        'expectedusers' => [
                            'a',
                            'b',
                            'c',
                            'd',
                            'e',
                            'f',
                            'g',
                            'h',
                        ],
                    ],
                    'NONE: Filter on student' => (object) [
                        'roles' => ['student'],
                        'jointype' => filter::JOINTYPE_NONE,
                        'count' => 6,
                        'expectedusers' => [
                            'c',
                            'd',
                            'e',
                            'f',
                            'g',
                            'h',
                        ],
                    ],
                    'NONE: Filter on teacher' => (object) [
                        'roles' => ['teacher'],
                        'jointype' => filter::JOINTYPE_NONE,
                        'count' => 5,
                        'expectedusers' => [
                            'a',
                            'c',
                            'd',
                            'g',
                            'h',
                        ],
                    ],
                    'NONE: Filter on editingteacher' => (object) [
                        'roles' => ['editingteacher'],
                        'jointype' => filter::JOINTYPE_NONE,
                        'count' => 5,
                        'expectedusers' => [
                            'a',
                            'b',
                            'f',
                            'g',
                            'h',
                        ],
                    ],
                    'NONE: Filter on student, teacher' => (object) [
                        'roles' => ['student', 'teacher'],
                        'jointype' => filter::JOINTYPE_NONE,
                        'count' => 5,
                        'expectedusers' => [
                            'c',
                            'd',
                            'e',
                            'g',
                            'h',
                        ],
                    ],
                    'NONE: Filter on student, teacher' => (object) [
                        'roles' => ['teacher', 'editingteacher'],
                        'jointype' => filter::JOINTYPE_NONE,
                        'count' => 3,
                        'expectedusers' => [
                            'a',
                            'g',
                            'h',
                        ],
                    ],
                    'NONE: Filter on student, manager (category level role)' => (object) [
                        'roles' => ['student', 'manager'],
                        'jointype' => filter::JOINTYPE_NONE,
                        'count' => 5,
                        'expectedusers' => [
                            'c',
                            'd',
                            'e',
                            'f',
                            'g',
                        ],
                    ],
                    'NONE: Filter on student, coursecreator (not assigned)' => (object) [
                        'roles' => ['student', 'coursecreator'],
                        'jointype' => filter::JOINTYPE_NONE,
                        'count' => 6,
                        'expectedusers' => [
                            'c',
                            'd',
                            'e',
                            'f',
                            'g',
                            'h',
                        ],
                    ],
                ],
            ],
        ];

        $finaltests = [];
        foreach ($tests as $testname => $testdata) {
            foreach ($testdata->expect as $expectname => $expectdata) {
                $finaltests["{$testname} => {$expectname}"] = [
                    'users' => $testdata->users,
                    'roles' => $expectdata->roles,
                    'jointype' => $expectdata->jointype,
                    'count' => $expectdata->count,
                    'expectedusers' => $expectdata->expectedusers,
                ];
            }
        }

        return $finaltests;
    }

    /**
     * Test participant search country filter
     *
     * @param array $usersdata
     * @param array $countries
     * @param int $jointype
     * @param array $expectedusers
     *
     * @dataProvider country_provider
     */
    public function test_country_filter(array $usersdata, array $countries, int $jointype, array $expectedusers): void {
        $this->resetAfterTest();

        $course = $this->getDataGenerator()->create_course();
        $users = [];

        foreach ($usersdata as $username => $country) {
            $users[$username] = $this->getDataGenerator()->create_and_enrol($course, 'student', (object) [
                'username' => $username,
                'country' => $country,
            ]);
        }

        // Add filters (courseid is required).
        $filterset = new participants_filterset();
        $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id]));
        $filterset->add_filter(new string_filter('country', $jointype, $countries));

        // Run the search, assert count matches the number of expected users.
        $search = new participants_search($course, context_course::instance($course->id), $filterset);
        $this->assertEquals(count($expectedusers), $search->get_total_participants_count());

        $rs = $search->get_participants();
        $this->assertInstanceOf(moodle_recordset::class, $rs);

        // Assert that each expected user is within the participant records.
        $records = $this->convert_recordset_to_array($rs);
        foreach ($expectedusers as $expecteduser) {
            $this->assertArrayHasKey($users[$expecteduser]->id, $records);
        }
    }

    /**
     * Data provider for {@see test_country_filter}
     *
     * @return array
     */
    public function country_provider(): array {
        $tests = [
            'users' => [
                'user1' => 'DE',
                'user2' => 'ES',
                'user3' => 'ES',
                'user4' => 'GB',
            ],
            'expects' => [
                // Tests for jointype: ANY.
                'ANY: No filter' => (object) [
                    'countries' => [],
                    'jointype' => filter::JOINTYPE_ANY,
                    'expectedusers' => [
                        'user1',
                        'user2',
                        'user3',
                        'user4',
                    ],
                ],
                'ANY: Matching filters' => (object) [
                    'countries' => [
                        'DE',
                        'GB',
                    ],
                    'jointype' => filter::JOINTYPE_ANY,
                    'expectedusers' => [
                        'user1',
                        'user4',
                    ],
                ],
                'ANY: Non-matching filters' => (object) [
                    'countries' => [
                        'RU',
                    ],
                    'jointype' => filter::JOINTYPE_ANY,
                    'expectedusers' => [],
                ],

                // Tests for jointype: ALL.
                'ALL: No filter' => (object) [
                    'countries' => [],
                    'jointype' => filter::JOINTYPE_ALL,
                    'expectedusers' => [
                        'user1',
                        'user2',
                        'user3',
                        'user4',
                    ],
                ],
                'ALL: Matching filters' => (object) [
                    'countries' => [
                        'DE',
                        'GB',
                    ],
                    'jointype' => filter::JOINTYPE_ALL,
                    'expectedusers' => [
                        'user1',
                        'user4',
                    ],
                ],
                'ALL: Non-matching filters' => (object) [
                    'countries' => [
                        'RU',
                    ],
                    'jointype' => filter::JOINTYPE_ALL,
                    'expectedusers' => [],
                ],

                // Tests for jointype: NONE.
                'NONE: No filter' => (object) [
                    'countries' => [],
                    'jointype' => filter::JOINTYPE_NONE,
                    'expectedusers' => [
                        'user1',
                        'user2',
                        'user3',
                        'user4',
                    ],
                ],
                'NONE: Matching filters' => (object) [
                    'countries' => [
                        'DE',
                        'GB',
                    ],
                    'jointype' => filter::JOINTYPE_NONE,
                    'expectedusers' => [
                        'user2',
                        'user3',
                    ],
                ],
                'NONE: Non-matching filters' => (object) [
                    'countries' => [
                        'RU',
                    ],
                    'jointype' => filter::JOINTYPE_NONE,
                    'expectedusers' => [
                        'user1',
                        'user2',
                        'user3',
                        'user4',
                    ],
                ],
            ],
        ];

        $finaltests = [];
        foreach ($tests['expects'] as $testname => $test) {
            $finaltests[$testname] = [
                'users' => $tests['users'],
                'countries' => $test->countries,
                'jointype' => $test->jointype,
                'expectedusers' => $test->expectedusers,
            ];
        }

        return $finaltests;
    }

    /**
     * Ensure that the keywords filter works as expected with the provided test cases.
     *
     * @param array $usersdata The list of users to create
     * @param array $keywords The list of keywords to filter by
     * @param int $jointype The join type to use when combining filter values
     * @param int $count The expected count
     * @param array $expectedusers
     * @param string $asuser If non-blank, uses that user account (for identify field permission checks)
     * @dataProvider keywords_provider
     */
    public function test_keywords_filter(array $usersdata, array $keywords, int $jointype, int $count,
            array $expectedusers, string $asuser): void {
        global $DB;

        $course = $this->getDataGenerator()->create_course();
        $coursecontext = context_course::instance($course->id);
        $users = [];

        // Create the custom user profile field and put it into showuseridentity.
        $this->getDataGenerator()->create_custom_profile_field(
                ['datatype' => 'text', 'shortname' => 'frog', 'name' => 'Fave frog']);
        set_config('showuseridentity', 'email,profile_field_frog');

        foreach ($usersdata as $username => $userdata) {
            // Prevent randomly generated field values that may cause false fails.
            $userdata['firstnamephonetic'] = $userdata['firstnamephonetic'] ?? $userdata['firstname'];
            $userdata['lastnamephonetic'] = $userdata['lastnamephonetic'] ?? $userdata['lastname'];
            $userdata['middlename'] = $userdata['middlename'] ?? '';
            $userdata['alternatename'] = $userdata['alternatename'] ?? $username;

            $user = $this->getDataGenerator()->create_user($userdata);
            $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
            $users[$username] = $user;
        }

        // Create a secondary course with users. We should not see these users.
        $this->create_course_with_users(10, 10, 10, 10);

        // Create the basic filter.
        $filterset = new participants_filterset();
        $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id]));

        // Create the keyword filter.
        $keywordfilter = new string_filter('keywords');
        $filterset->add_filter($keywordfilter);

        // Configure the filter.
        foreach ($keywords as $keyword) {
            $keywordfilter->add_filter_value($keyword);
        }
        $keywordfilter->set_join_type($jointype);

        if ($asuser) {
            $this->setUser($DB->get_record('user', ['username' => $asuser]));
        }

        // Run the search.
        $search = new participants_search($course, $coursecontext, $filterset);
        $rs = $search->get_participants();
        $this->assertInstanceOf(moodle_recordset::class, $rs);
        $records = $this->convert_recordset_to_array($rs);

        $this->assertCount($count, $records);
        $this->assertEquals($count, $search->get_total_participants_count());

        foreach ($expectedusers as $expecteduser) {
            $this->assertArrayHasKey($users[$expecteduser]->id, $records);
        }
    }

    /**
     * Data provider for keywords tests.
     *
     * @return array
     */
    public function keywords_provider(): array {
        $tests = [
            // Users where the keyword matches basic user fields such as names and email.
            'Users with basic names' => (object) [
                'users' => [
                    'adam.ant' => [
                        'firstname' => 'Adam',
                        'lastname' => 'Ant',
                    ],
                    'barbara.bennett' => [
                        'firstname' => 'Barbara',
                        'lastname' => 'Bennett',
                        'alternatename' => 'Babs',
                        'firstnamephonetic' => 'Barbra',
                        'lastnamephonetic' => 'Benit',
                        'profile_field_frog' => 'Kermit',
                    ],
                    'colin.carnforth' => [
                        'firstname' => 'Colin',
                        'lastname' => 'Carnforth',
                        'middlename' => 'Jeffery',
                    ],
                    'tony.rogers' => [
                        'firstname' => 'Anthony',
                        'lastname' => 'Rogers',
                        'lastnamephonetic' => 'Rowjours',
                        'profile_field_frog' => 'Mr Toad',
                    ],
                    'sarah.rester' => [
                        'firstname' => 'Sarah',
                        'lastname' => 'Rester',
                        'email' => 'zazu@example.com',
                        'firstnamephonetic' => 'Sera',
                    ],
                ],
                'expect' => [
                    // Tests for jointype: ANY.
                    'ANY: No filter' => (object) [
                        'keywords' => [],
                        'jointype' => filter::JOINTYPE_ANY,
                        'count' => 5,
                        'expectedusers' => [
                            'adam.ant',
                            'barbara.bennett',
                            'colin.carnforth',
                            'tony.rogers',
                            'sarah.rester',
                        ],
                    ],
                    'ANY: Filter on first name only' => (object) [
                        'keywords' => ['adam'],
                        'jointype' => filter::JOINTYPE_ANY,
                        'count' => 1,
                        'expectedusers' => [
                            'adam.ant',
                        ],
                    ],
                    'ANY: Filter on last name only' => (object) [
                        'keywords' => ['BeNNeTt'],
                        'jointype' => filter::JOINTYPE_ANY,
                        'count' => 1,
                        'expectedusers' => [
                            'barbara.bennett',
                        ],
                    ],
                    'ANY: Filter on first/Last name' => (object) [
                        'keywords' => ['ant'],
                        'jointype' => filter::JOINTYPE_ANY,
                        'count' => 2,
                        'expectedusers' => [
                            'adam.ant',
                            'tony.rogers',
                        ],
                    ],
> 'ANY: Filter on fullname only' => (object) [ 'ANY: Filter on middlename only' => (object) [ > 'keywords' => ['Barbara Bennett'], 'keywords' => ['Jeff'], > 'jointype' => filter::JOINTYPE_ANY, 'jointype' => filter::JOINTYPE_ANY, > 'count' => 1, 'count' => 1, > 'expectedusers' => [ 'expectedusers' => [ > 'barbara.bennett', 'colin.carnforth', > ], ], > ],
], 'ANY: Filter on username (no match)' => (object) [ 'keywords' => ['sara.rester'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 0, 'expectedusers' => [], ], 'ANY: Filter on email only' => (object) [ 'keywords' => ['zazu'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 1, 'expectedusers' => [ 'sarah.rester', ], ], 'ANY: Filter on first name phonetic only' => (object) [ 'keywords' => ['Sera'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 1, 'expectedusers' => [ 'sarah.rester', ], ], 'ANY: Filter on last name phonetic only' => (object) [ 'keywords' => ['jour'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 1, 'expectedusers' => [ 'tony.rogers', ], ], 'ANY: Filter on alternate name only' => (object) [ 'keywords' => ['Babs'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 1, 'expectedusers' => [ 'barbara.bennett', ], ], 'ANY: Filter on multiple keywords (first/middle/last name)' => (object) [ 'keywords' => ['ant', 'Jeff', 'rog'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 3, 'expectedusers' => [ 'adam.ant', 'colin.carnforth', 'tony.rogers', ], ], 'ANY: Filter on multiple keywords (phonetic/alternate names)' => (object) [ 'keywords' => ['era', 'Bab', 'ours'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 3, 'expectedusers' => [ 'barbara.bennett', 'sarah.rester', 'tony.rogers', ], ], 'ANY: Filter on custom profile field' => (object) [ 'keywords' => ['Kermit', 'Mr Toad'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 2, 'expectedusers' => [ 'barbara.bennett', 'tony.rogers', ], 'asuser' => 'admin' ], 'ANY: Filter on custom profile field (no permissions)' => (object) [ 'keywords' => ['Kermit', 'Mr Toad'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 0, 'expectedusers' => [], 'asuser' => 'barbara.bennett' ], // Tests for jointype: ALL. 'ALL: No filter' => (object) [ 'keywords' => [], 'jointype' => filter::JOINTYPE_ALL, 'count' => 5, 'expectedusers' => [ 'adam.ant', 'barbara.bennett', 'colin.carnforth', 'tony.rogers', 'sarah.rester', ], ], 'ALL: Filter on first name only' => (object) [ 'keywords' => ['adam'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 1, 'expectedusers' => [ 'adam.ant', ], ], 'ALL: Filter on last name only' => (object) [ 'keywords' => ['BeNNeTt'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 1, 'expectedusers' => [ 'barbara.bennett', ], ], 'ALL: Filter on first/Last name' => (object) [ 'keywords' => ['ant'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 2, 'expectedusers' => [ 'adam.ant', 'tony.rogers', ], ], 'ALL: Filter on middlename only' => (object) [ 'keywords' => ['Jeff'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 1, 'expectedusers' => [ 'colin.carnforth', ], ], 'ALL: Filter on username (no match)' => (object) [ 'keywords' => ['sara.rester'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 0, 'expectedusers' => [], ], 'ALL: Filter on email only' => (object) [ 'keywords' => ['zazu'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 1, 'expectedusers' => [ 'sarah.rester', ], ], 'ALL: Filter on first name phonetic only' => (object) [ 'keywords' => ['Sera'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 1, 'expectedusers' => [ 'sarah.rester', ], ], 'ALL: Filter on last name phonetic only' => (object) [ 'keywords' => ['jour'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 1, 'expectedusers' => [ 'tony.rogers', ], ], 'ALL: Filter on alternate name only' => (object) [ 'keywords' => ['Babs'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 1, 'expectedusers' => [ 'barbara.bennett', ], ], 'ALL: Filter on multiple keywords (first/last name)' => (object) [ 'keywords' => ['ant', 'rog'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 1, 'expectedusers' => [ 'tony.rogers', ], ], 'ALL: Filter on multiple keywords (first/middle/last name)' => (object) [ 'keywords' => ['ant', 'Jeff', 'rog'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 0, 'expectedusers' => [], ], 'ALL: Filter on multiple keywords (phonetic/alternate names)' => (object) [ 'keywords' => ['Bab', 'bra', 'nit'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 1, 'expectedusers' => [ 'barbara.bennett', ], ], 'ALL: Filter on custom profile field' => (object) [ 'keywords' => ['Kermit', 'Kermi'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 1, 'expectedusers' => [ 'barbara.bennett', ], 'asuser' => 'admin', ], 'ALL: Filter on custom profile field (no permissions)' => (object) [ 'keywords' => ['Kermit', 'Kermi'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 0, 'expectedusers' => [], 'asuser' => 'barbara.bennett', ], // Tests for jointype: NONE. 'NONE: No filter' => (object) [ 'keywords' => [], 'jointype' => filter::JOINTYPE_NONE, 'count' => 5, 'expectedusers' => [ 'adam.ant', 'barbara.bennett', 'colin.carnforth', 'tony.rogers', 'sarah.rester', ], ], 'NONE: Filter on first name only' => (object) [ 'keywords' => ['ara'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 3, 'expectedusers' => [ 'adam.ant', 'colin.carnforth', 'tony.rogers', ], ], 'NONE: Filter on last name only' => (object) [ 'keywords' => ['BeNNeTt'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 4, 'expectedusers' => [ 'adam.ant', 'colin.carnforth', 'tony.rogers', 'sarah.rester', ], ], 'NONE: Filter on first/Last name' => (object) [ 'keywords' => ['ar'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 2, 'expectedusers' => [ 'adam.ant', 'tony.rogers', ], ], 'NONE: Filter on middlename only' => (object) [ 'keywords' => ['Jeff'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 4, 'expectedusers' => [ 'adam.ant', 'barbara.bennett', 'tony.rogers', 'sarah.rester', ], ], 'NONE: Filter on username (no match)' => (object) [ 'keywords' => ['sara.rester'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 5, 'expectedusers' => [ 'adam.ant', 'barbara.bennett', 'colin.carnforth', 'tony.rogers', 'sarah.rester', ], ], 'NONE: Filter on email' => (object) [ 'keywords' => ['zazu'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 4, 'expectedusers' => [ 'adam.ant', 'barbara.bennett', 'colin.carnforth', 'tony.rogers', ], ], 'NONE: Filter on first name phonetic only' => (object) [ 'keywords' => ['Sera'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 4, 'expectedusers' => [ 'adam.ant', 'barbara.bennett', 'colin.carnforth', 'tony.rogers', ], ], 'NONE: Filter on last name phonetic only' => (object) [ 'keywords' => ['jour'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 4, 'expectedusers' => [ 'adam.ant', 'barbara.bennett', 'colin.carnforth', 'sarah.rester', ], ], 'NONE: Filter on alternate name only' => (object) [ 'keywords' => ['Babs'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 4, 'expectedusers' => [ 'adam.ant', 'colin.carnforth', 'tony.rogers', 'sarah.rester', ], ], 'NONE: Filter on multiple keywords (first/last name)' => (object) [ 'keywords' => ['ara', 'rog'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 2, 'expectedusers' => [ 'adam.ant', 'colin.carnforth', ], ], 'NONE: Filter on multiple keywords (first/middle/last name)' => (object) [ 'keywords' => ['ant', 'Jeff', 'rog'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 2, 'expectedusers' => [ 'barbara.bennett', 'sarah.rester', ], ], 'NONE: Filter on multiple keywords (phonetic/alternate names)' => (object) [ 'keywords' => ['Bab', 'bra', 'nit'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 4, 'expectedusers' => [ 'adam.ant', 'colin.carnforth', 'tony.rogers', 'sarah.rester', ], ], 'NONE: Filter on custom profile field' => (object) [ 'keywords' => ['Kermit', 'Mr Toad'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 3, 'expectedusers' => [ 'adam.ant', 'colin.carnforth', 'sarah.rester', ], 'asuser' => 'admin', ], 'NONE: Filter on custom profile field (no permissions)' => (object) [ 'keywords' => ['Kermit', 'Mr Toad'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 5, 'expectedusers' => [ 'adam.ant', 'barbara.bennett', 'colin.carnforth', 'tony.rogers', 'sarah.rester', ], 'asuser' => 'barbara.bennett', ], ], ], ]; $finaltests = []; foreach ($tests as $testname => $testdata) { foreach ($testdata->expect as $expectname => $expectdata) { $finaltests["{$testname} => {$expectname}"] = [ 'users' => $testdata->users, 'keywords' => $expectdata->keywords, 'jointype' => $expectdata->jointype, 'count' => $expectdata->count, 'expectedusers' => $expectdata->expectedusers, 'asuser' => $expectdata->asuser ?? '' ]; } } return $finaltests; } /** * Ensure that the enrolment status filter works as expected with the provided test cases. * * @param array $usersdata The list of users to create * @param array $statuses The list of statuses to filter by * @param int $jointype The join type to use when combining filter values * @param int $count The expected count * @param array $expectedusers * @dataProvider status_provider */ public function test_status_filter(array $usersdata, array $statuses, int $jointype, int $count, array $expectedusers): void { $course = $this->getDataGenerator()->create_course(); $coursecontext = context_course::instance($course->id); $users = []; // Ensure sufficient capabilities to view all statuses. $this->setAdminUser(); // Ensure all enrolment methods enabled. $enrolinstances = enrol_get_instances($course->id, false); foreach ($enrolinstances as $instance) { $plugin = enrol_get_plugin($instance->enrol); $plugin->update_status($instance, ENROL_INSTANCE_ENABLED); } foreach ($usersdata as $username => $userdata) { $user = $this->getDataGenerator()->create_user(['username' => $username]); if (array_key_exists('status', $userdata)) { foreach ($userdata['status'] as $enrolmethod => $status) { $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student', $enrolmethod, 0, 0, $status); } } $users[$username] = $user; } // Create a secondary course with users. We should not see these users. $this->create_course_with_users(1, 1, 1, 1); // Create the basic filter. $filterset = new participants_filterset(); $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id])); // Create the status filter. $statusfilter = new integer_filter('status'); $filterset->add_filter($statusfilter); // Configure the filter. foreach ($statuses as $status) { $statusfilter->add_filter_value($status); } $statusfilter->set_join_type($jointype); // Run the search. $search = new participants_search($course, $coursecontext, $filterset); $rs = $search->get_participants(); $this->assertInstanceOf(moodle_recordset::class, $rs); $records = $this->convert_recordset_to_array($rs); $this->assertCount($count, $records); $this->assertEquals($count, $search->get_total_participants_count()); foreach ($expectedusers as $expecteduser) { $this->assertArrayHasKey($users[$expecteduser]->id, $records); } } /** * Data provider for status filter tests. * * @return array */ public function status_provider(): array { $tests = [ // Users with different statuses and enrolment methods (so multiple statuses are possible for the same user). 'Users with different enrolment statuses' => (object) [ 'users' => [ 'a' => [ 'status' => [ 'manual' => ENROL_USER_ACTIVE, ] ], 'b' => [ 'status' => [ 'self' => ENROL_USER_ACTIVE, ] ], 'c' => [ 'status' => [ 'manual' => ENROL_USER_SUSPENDED, ] ], 'd' => [ 'status' => [ 'self' => ENROL_USER_SUSPENDED, ] ], 'e' => [ 'status' => [ 'manual' => ENROL_USER_ACTIVE, 'self' => ENROL_USER_SUSPENDED, ] ], ], 'expect' => [ // Tests for jointype: ANY. 'ANY: No filter' => (object) [ 'status' => [], 'jointype' => filter::JOINTYPE_ANY, 'count' => 5, 'expectedusers' => [ 'a', 'b', 'c', 'd', 'e', ], ], 'ANY: Filter on active only' => (object) [ 'status' => [ENROL_USER_ACTIVE], 'jointype' => filter::JOINTYPE_ANY, 'count' => 3, 'expectedusers' => [ 'a', 'b', 'e', ], ], 'ANY: Filter on suspended only' => (object) [ 'status' => [ENROL_USER_SUSPENDED], 'jointype' => filter::JOINTYPE_ANY, 'count' => 3, 'expectedusers' => [ 'c', 'd', 'e', ], ], 'ANY: Filter on multiple statuses' => (object) [ 'status' => [ENROL_USER_ACTIVE, ENROL_USER_SUSPENDED], 'jointype' => filter::JOINTYPE_ANY, 'count' => 5, 'expectedusers' => [ 'a', 'b', 'c', 'd', 'e', ], ], // Tests for jointype: ALL. 'ALL: No filter' => (object) [ 'status' => [], 'jointype' => filter::JOINTYPE_ALL, 'count' => 5, 'expectedusers' => [ 'a', 'b', 'c', 'd', 'e', ], ], 'ALL: Filter on active only' => (object) [ 'status' => [ENROL_USER_ACTIVE], 'jointype' => filter::JOINTYPE_ALL, 'count' => 3, 'expectedusers' => [ 'a', 'b', 'e', ], ], 'ALL: Filter on suspended only' => (object) [ 'status' => [ENROL_USER_SUSPENDED], 'jointype' => filter::JOINTYPE_ALL, 'count' => 3, 'expectedusers' => [ 'c', 'd', 'e', ], ], 'ALL: Filter on multiple statuses' => (object) [ 'status' => [ENROL_USER_ACTIVE, ENROL_USER_SUSPENDED], 'jointype' => filter::JOINTYPE_ALL, 'count' => 1, 'expectedusers' => [ 'e', ], ], // Tests for jointype: NONE. 'NONE: No filter' => (object) [ 'status' => [], 'jointype' => filter::JOINTYPE_NONE, 'count' => 5, 'expectedusers' => [ 'a', 'b', 'c', 'd', 'e', ], ], 'NONE: Filter on active only' => (object) [ 'status' => [ENROL_USER_ACTIVE], 'jointype' => filter::JOINTYPE_NONE, 'count' => 3, 'expectedusers' => [ 'c', 'd', 'e', ], ], 'NONE: Filter on suspended only' => (object) [ 'status' => [ENROL_USER_SUSPENDED], 'jointype' => filter::JOINTYPE_NONE, 'count' => 3, 'expectedusers' => [ 'a', 'b', 'e', ], ], 'NONE: Filter on multiple statuses' => (object) [ 'status' => [ENROL_USER_ACTIVE, ENROL_USER_SUSPENDED], 'jointype' => filter::JOINTYPE_NONE, 'count' => 0, 'expectedusers' => [], ], ], ], ]; $finaltests = []; foreach ($tests as $testname => $testdata) { foreach ($testdata->expect as $expectname => $expectdata) { $finaltests["{$testname} => {$expectname}"] = [ 'users' => $testdata->users, 'status' => $expectdata->status, 'jointype' => $expectdata->jointype, 'count' => $expectdata->count, 'expectedusers' => $expectdata->expectedusers, ]; } } return $finaltests; } /** * Ensure that the enrolment methods filter works as expected with the provided test cases. * * @param array $usersdata The list of users to create * @param array $enrolmethods The list of enrolment methods to filter by * @param int $jointype The join type to use when combining filter values * @param int $count The expected count * @param array $expectedusers * @dataProvider enrolments_provider */ public function test_enrolments_filter(array $usersdata, array $enrolmethods, int $jointype, int $count, array $expectedusers): void { $course = $this->getDataGenerator()->create_course(); $coursecontext = context_course::instance($course->id); $users = []; // Ensure all enrolment methods enabled and mapped for setting the filter later. $enrolinstances = enrol_get_instances($course->id, false); $enrolinstancesmap = []; foreach ($enrolinstances as $instance) { $plugin = enrol_get_plugin($instance->enrol); $plugin->update_status($instance, ENROL_INSTANCE_ENABLED); $enrolinstancesmap[$instance->enrol] = (int) $instance->id; } foreach ($usersdata as $username => $userdata) { $user = $this->getDataGenerator()->create_user(['username' => $username]); if (array_key_exists('enrolmethods', $userdata)) { foreach ($userdata['enrolmethods'] as $enrolmethod) { $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student', $enrolmethod); } } $users[$username] = $user; } // Create a secondary course with users. We should not see these users. $this->create_course_with_users(1, 1, 1, 1); // Create the basic filter. $filterset = new participants_filterset(); $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id])); // Create the enrolment methods filter. $enrolmethodfilter = new integer_filter('enrolments'); $filterset->add_filter($enrolmethodfilter); // Configure the filter. foreach ($enrolmethods as $enrolmethod) { $enrolmethodfilter->add_filter_value($enrolinstancesmap[$enrolmethod]); } $enrolmethodfilter->set_join_type($jointype); // Run the search. $search = new participants_search($course, $coursecontext, $filterset); $rs = $search->get_participants(); $this->assertInstanceOf(moodle_recordset::class, $rs); $records = $this->convert_recordset_to_array($rs); $this->assertCount($count, $records); $this->assertEquals($count, $search->get_total_participants_count()); foreach ($expectedusers as $expecteduser) { $this->assertArrayHasKey($users[$expecteduser]->id, $records); } } /** * Data provider for enrolments filter tests. * * @return array */ public function enrolments_provider(): array { $tests = [ // Users with different enrolment methods. 'Users with different enrolment methods' => (object) [ 'users' => [ 'a' => [ 'enrolmethods' => [ 'manual', ] ], 'b' => [ 'enrolmethods' => [ 'self', ] ], 'c' => [ 'enrolmethods' => [ 'manual', 'self', ] ], ], 'expect' => [ // Tests for jointype: ANY. 'ANY: No filter' => (object) [ 'enrolmethods' => [], 'jointype' => filter::JOINTYPE_ANY, 'count' => 3, 'expectedusers' => [ 'a', 'b', 'c', ], ], 'ANY: Filter by manual enrolments only' => (object) [ 'enrolmethods' => ['manual'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 2, 'expectedusers' => [ 'a', 'c', ], ], 'ANY: Filter by self enrolments only' => (object) [ 'enrolmethods' => ['self'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 2, 'expectedusers' => [ 'b', 'c', ], ], 'ANY: Filter by multiple enrolment methods' => (object) [ 'enrolmethods' => ['manual', 'self'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 3, 'expectedusers' => [ 'a', 'b', 'c', ], ], // Tests for jointype: ALL. 'ALL: No filter' => (object) [ 'enrolmethods' => [], 'jointype' => filter::JOINTYPE_ALL, 'count' => 3, 'expectedusers' => [ 'a', 'b', 'c', ], ], 'ALL: Filter by manual enrolments only' => (object) [ 'enrolmethods' => ['manual'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 2, 'expectedusers' => [ 'a', 'c', ], ], 'ALL: Filter by multiple enrolment methods' => (object) [ 'enrolmethods' => ['manual', 'self'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 1, 'expectedusers' => [ 'c', ], ], // Tests for jointype: NONE. 'NONE: No filter' => (object) [ 'enrolmethods' => [], 'jointype' => filter::JOINTYPE_NONE, 'count' => 3, 'expectedusers' => [ 'a', 'b', 'c', ], ], 'NONE: Filter by manual enrolments only' => (object) [ 'enrolmethods' => ['manual'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 1, 'expectedusers' => [ 'b', ], ], 'NONE: Filter by multiple enrolment methods' => (object) [ 'enrolmethods' => ['manual', 'self'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 0, 'expectedusers' => [], ], ], ], ]; $finaltests = []; foreach ($tests as $testname => $testdata) { foreach ($testdata->expect as $expectname => $expectdata) { $finaltests["{$testname} => {$expectname}"] = [ 'users' => $testdata->users, 'enrolmethods' => $expectdata->enrolmethods, 'jointype' => $expectdata->jointype, 'count' => $expectdata->count, 'expectedusers' => $expectdata->expectedusers, ]; } } return $finaltests; } /** * Ensure that the groups filter works as expected with the provided test cases. * * @param array $usersdata The list of users to create * @param array $groupsavailable The names of groups that should be created in the course * @param array $filtergroups The names of groups to filter by * @param int $jointype The join type to use when combining filter values * @param int $count The expected count * @param array $expectedusers * @dataProvider groups_provider */ public function test_groups_filter(array $usersdata, array $groupsavailable, array $filtergroups, int $jointype, int $count, array $expectedusers): void { $course = $this->getDataGenerator()->create_course(); $coursecontext = context_course::instance($course->id); $users = []; // Prepare data for filtering by users in no groups. $nogroupsdata = (object) [ 'id' => USERSWITHOUTGROUP, ]; // Map group names to group data. $groupsdata = ['nogroups' => $nogroupsdata]; foreach ($groupsavailable as $groupname) { $groupinfo = [ 'courseid' => $course->id, 'name' => $groupname, ]; $groupsdata[$groupname] = $this->getDataGenerator()->create_group($groupinfo); } foreach ($usersdata as $username => $userdata) { $user = $this->getDataGenerator()->create_user(['username' => $username]); $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student'); if (array_key_exists('groups', $userdata)) { foreach ($userdata['groups'] as $groupname) { $userinfo = [ 'userid' => $user->id, 'groupid' => (int) $groupsdata[$groupname]->id, ]; $this->getDataGenerator()->create_group_member($userinfo); } } $users[$username] = $user; } // Create a secondary course with users. We should not see these users. $this->create_course_with_users(1, 1, 1, 1); // Create the basic filter. $filterset = new participants_filterset(); $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id])); // Create the groups filter. $groupsfilter = new integer_filter('groups'); $filterset->add_filter($groupsfilter); // Configure the filter. foreach ($filtergroups as $filtergroupname) { $groupsfilter->add_filter_value((int) $groupsdata[$filtergroupname]->id); } $groupsfilter->set_join_type($jointype); // Run the search. $search = new participants_search($course, $coursecontext, $filterset); $rs = $search->get_participants(); $this->assertInstanceOf(moodle_recordset::class, $rs); $records = $this->convert_recordset_to_array($rs); $this->assertCount($count, $records); $this->assertEquals($count, $search->get_total_participants_count()); foreach ($expectedusers as $expecteduser) { $this->assertArrayHasKey($users[$expecteduser]->id, $records); } } /** * Data provider for groups filter tests. * * @return array */ public function groups_provider(): array { $tests = [ 'Users in different groups' => (object) [ 'groupsavailable' => [ 'groupa', 'groupb', 'groupc', ], 'users' => [ 'a' => [ 'groups' => ['groupa'], ], 'b' => [ 'groups' => ['groupb'], ], 'c' => [ 'groups' => ['groupa', 'groupb'], ], 'd' => [ 'groups' => [], ], ], 'expect' => [ // Tests for jointype: ANY. 'ANY: No filter' => (object) [ 'groups' => [], 'jointype' => filter::JOINTYPE_ANY, 'count' => 4, 'expectedusers' => [ 'a', 'b', 'c', 'd', ], ], 'ANY: Filter on a single group' => (object) [ 'groups' => ['groupa'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 2, 'expectedusers' => [ 'a', 'c', ], ], 'ANY: Filter on a group with no members' => (object) [ 'groups' => ['groupc'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 0, 'expectedusers' => [], ], 'ANY: Filter on multiple groups' => (object) [ 'groups' => ['groupa', 'groupb'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 3, 'expectedusers' => [ 'a', 'b', 'c', ], ], 'ANY: Filter on members of no groups only' => (object) [ 'groups' => ['nogroups'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 1, 'expectedusers' => [ 'd', ], ], 'ANY: Filter on a single group or no groups' => (object) [ 'groups' => ['groupa', 'nogroups'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 3, 'expectedusers' => [ 'a', 'c', 'd', ], ], 'ANY: Filter on multiple groups or no groups' => (object) [ 'groups' => ['groupa', 'groupb', 'nogroups'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 4, 'expectedusers' => [ 'a', 'b', 'c', 'd', ], ], // Tests for jointype: ALL. 'ALL: No filter' => (object) [ 'groups' => [], 'jointype' => filter::JOINTYPE_ALL, 'count' => 4, 'expectedusers' => [ 'a', 'b', 'c', 'd', ], ], 'ALL: Filter on a single group' => (object) [ 'groups' => ['groupa'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 2, 'expectedusers' => [ 'a', 'c', ], ], 'ALL: Filter on a group with no members' => (object) [ 'groups' => ['groupc'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 0, 'expectedusers' => [], ], 'ALL: Filter on members of no groups only' => (object) [ 'groups' => ['nogroups'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 1, 'expectedusers' => [ 'd', ], ], 'ALL: Filter on multiple groups' => (object) [ 'groups' => ['groupa', 'groupb'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 1, 'expectedusers' => [ 'c', ], ], 'ALL: Filter on a single group and no groups' => (object) [ 'groups' => ['groupa', 'nogroups'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 0, 'expectedusers' => [], ], 'ALL: Filter on multiple groups and no groups' => (object) [ 'groups' => ['groupa', 'groupb', 'nogroups'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 0, 'expectedusers' => [], ], // Tests for jointype: NONE. 'NONE: No filter' => (object) [ 'groups' => [], 'jointype' => filter::JOINTYPE_NONE, 'count' => 4, 'expectedusers' => [ 'a', 'b', 'c', 'd', ], ], 'NONE: Filter on a single group' => (object) [ 'groups' => ['groupa'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 2, 'expectedusers' => [ 'b', 'd', ], ], 'NONE: Filter on a group with no members' => (object) [ 'groups' => ['groupc'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 4, 'expectedusers' => [ 'a', 'b', 'c', 'd', ], ], 'NONE: Filter on members of no groups only' => (object) [ 'groups' => ['nogroups'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 3, 'expectedusers' => [ 'a', 'b', 'c', ], ], 'NONE: Filter on multiple groups' => (object) [ 'groups' => ['groupa', 'groupb'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 1, 'expectedusers' => [ 'd', ], ], 'NONE: Filter on a single group and no groups' => (object) [ 'groups' => ['groupa', 'nogroups'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 1, 'expectedusers' => [ 'b', ], ], 'NONE: Filter on multiple groups and no groups' => (object) [ 'groups' => ['groupa', 'groupb', 'nogroups'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 0, 'expectedusers' => [], ], ], ], ]; $finaltests = []; foreach ($tests as $testname => $testdata) { foreach ($testdata->expect as $expectname => $expectdata) { $finaltests["{$testname} => {$expectname}"] = [ 'users' => $testdata->users, 'groupsavailable' => $testdata->groupsavailable, 'filtergroups' => $expectdata->groups, 'jointype' => $expectdata->jointype, 'count' => $expectdata->count, 'expectedusers' => $expectdata->expectedusers, ]; } } return $finaltests; } /** * Ensure that the groups filter works as expected when separate groups mode is enabled, with the provided test cases. * * @param array $usersdata The list of users to create * @param array $groupsavailable The names of groups that should be created in the course * @param array $filtergroups The names of groups to filter by * @param int $jointype The join type to use when combining filter values * @param int $count The expected count * @param array $expectedusers * @param string $loginusername The user to login as for the tests * @dataProvider groups_separate_provider */ public function test_groups_filter_separate_groups(array $usersdata, array $groupsavailable, array $filtergroups, int $jointype, int $count, array $expectedusers, string $loginusername): void { $course = $this->getDataGenerator()->create_course(); $coursecontext = context_course::instance($course->id); $users = []; // Enable separate groups mode on the course. $course->groupmode = SEPARATEGROUPS; $course->groupmodeforce = true; update_course($course); // Prepare data for filtering by users in no groups. $nogroupsdata = (object) [ 'id' => USERSWITHOUTGROUP, ]; // Map group names to group data. $groupsdata = ['nogroups' => $nogroupsdata]; foreach ($groupsavailable as $groupname) { $groupinfo = [ 'courseid' => $course->id, 'name' => $groupname, ]; $groupsdata[$groupname] = $this->getDataGenerator()->create_group($groupinfo); } foreach ($usersdata as $username => $userdata) { $user = $this->getDataGenerator()->create_user(['username' => $username]); $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student'); if (array_key_exists('groups', $userdata)) { foreach ($userdata['groups'] as $groupname) { $userinfo = [ 'userid' => $user->id, 'groupid' => (int) $groupsdata[$groupname]->id, ]; $this->getDataGenerator()->create_group_member($userinfo); } } $users[$username] = $user; if ($username == $loginusername) { $loginuser = $user; } } // Create a secondary course with users. We should not see these users. $this->create_course_with_users(1, 1, 1, 1); // Log in as the user to be tested. $this->setUser($loginuser); // Create the basic filter. $filterset = new participants_filterset(); $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id])); // Create the groups filter. $groupsfilter = new integer_filter('groups'); $filterset->add_filter($groupsfilter); // Configure the filter. foreach ($filtergroups as $filtergroupname) { $groupsfilter->add_filter_value((int) $groupsdata[$filtergroupname]->id); } $groupsfilter->set_join_type($jointype); // Run the search. $search = new participants_search($course, $coursecontext, $filterset); // Tests on user in no groups should throw an exception as they are not supported (participants are not visible to them). if (in_array('exception', $expectedusers)) { $this->expectException(\coding_exception::class); $rs = $search->get_participants(); } else { // All other cases are tested as normal. $rs = $search->get_participants(); $this->assertInstanceOf(moodle_recordset::class, $rs); $records = $this->convert_recordset_to_array($rs); $this->assertCount($count, $records); $this->assertEquals($count, $search->get_total_participants_count()); foreach ($expectedusers as $expecteduser) { $this->assertArrayHasKey($users[$expecteduser]->id, $records); } } } /** * Data provider for groups filter tests. * * @return array */ public function groups_separate_provider(): array { $tests = [ 'Users in different groups with separate groups mode enabled' => (object) [ 'groupsavailable' => [ 'groupa', 'groupb', 'groupc', ], 'users' => [ 'a' => [ 'groups' => ['groupa'], ], 'b' => [ 'groups' => ['groupb'], ], 'c' => [ 'groups' => ['groupa', 'groupb'], ], 'd' => [ 'groups' => [], ], ], 'expect' => [ // Tests for jointype: ANY. 'ANY: No filter, user in one group' => (object) [ 'loginuser' => 'a', 'groups' => [], 'jointype' => filter::JOINTYPE_ANY, 'count' => 2, 'expectedusers' => [ 'a', 'c', ], ], 'ANY: No filter, user in multiple groups' => (object) [ 'loginuser' => 'c', 'groups' => [], 'jointype' => filter::JOINTYPE_ANY, 'count' => 3, 'expectedusers' => [ 'a', 'b', 'c', ], ], 'ANY: No filter, user in no groups' => (object) [ 'loginuser' => 'd', 'groups' => [], 'jointype' => filter::JOINTYPE_ANY, 'count' => 0, 'expectedusers' => ['exception'], ], 'ANY: Filter on a single group, user in one group' => (object) [ 'loginuser' => 'a', 'groups' => ['groupa'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 2, 'expectedusers' => [ 'a', 'c', ], ], 'ANY: Filter on a single group, user in multple groups' => (object) [ 'loginuser' => 'c', 'groups' => ['groupa'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 2, 'expectedusers' => [ 'a', 'c', ], ], 'ANY: Filter on a single group, user in no groups' => (object) [ 'loginuser' => 'd', 'groups' => ['groupa'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 0, 'expectedusers' => ['exception'], ], 'ANY: Filter on multiple groups, user in one group (ignore invalid groups)' => (object) [ 'loginuser' => 'a', 'groups' => ['groupa', 'groupb'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 2, 'expectedusers' => [ 'a', 'c', ], ], 'ANY: Filter on multiple groups, user in multiple groups' => (object) [ 'loginuser' => 'c', 'groups' => ['groupa', 'groupb'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 3, 'expectedusers' => [ 'a', 'b', 'c', ], ], 'ANY: Filter on multiple groups or no groups, user in multiple groups (ignore no groups)' => (object) [ 'loginuser' => 'c', 'groups' => ['groupa', 'groupb', 'nogroups'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 3, 'expectedusers' => [ 'a', 'b', 'c', ], ], // Tests for jointype: ALL. 'ALL: No filter, user in one group' => (object) [ 'loginuser' => 'a', 'groups' => [], 'jointype' => filter::JOINTYPE_ALL, 'count' => 2, 'expectedusers' => [ 'a', 'c', ], ], 'ALL: No filter, user in multiple groups' => (object) [ 'loginuser' => 'c', 'groups' => [], 'jointype' => filter::JOINTYPE_ALL, 'count' => 3, 'expectedusers' => [ 'a', 'b', 'c', ], ], 'ALL: No filter, user in no groups' => (object) [ 'loginuser' => 'd', 'groups' => [], 'jointype' => filter::JOINTYPE_ALL, 'count' => 0, 'expectedusers' => ['exception'], ], 'ALL: Filter on a single group, user in one group' => (object) [ 'loginuser' => 'a', 'groups' => ['groupa'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 2, 'expectedusers' => [ 'a', 'c', ], ], 'ALL: Filter on a single group, user in multple groups' => (object) [ 'loginuser' => 'c', 'groups' => ['groupa'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 2, 'expectedusers' => [ 'a', 'c', ], ], 'ALL: Filter on a single group, user in no groups' => (object) [ 'loginuser' => 'd', 'groups' => ['groupa'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 0, 'expectedusers' => ['exception'], ], 'ALL: Filter on multiple groups, user in one group (ignore invalid groups)' => (object) [ 'loginuser' => 'a', 'groups' => ['groupa', 'groupb'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 2, 'expectedusers' => [ 'a', 'c', ], ], 'ALL: Filter on multiple groups, user in multiple groups' => (object) [ 'loginuser' => 'c', 'groups' => ['groupa', 'groupb'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 1, 'expectedusers' => [ 'c', ], ], 'ALL: Filter on multiple groups or no groups, user in multiple groups (ignore no groups)' => (object) [ 'loginuser' => 'c', 'groups' => ['groupa', 'groupb', 'nogroups'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 1, 'expectedusers' => [ 'c', ], ], // Tests for jointype: NONE. 'NONE: No filter, user in one group' => (object) [ 'loginuser' => 'a', 'groups' => [], 'jointype' => filter::JOINTYPE_NONE, 'count' => 2, 'expectedusers' => [ 'a', 'c', ], ], 'NONE: No filter, user in multiple groups' => (object) [ 'loginuser' => 'c', 'groups' => [], 'jointype' => filter::JOINTYPE_NONE, 'count' => 3, 'expectedusers' => [ 'a', 'b', 'c', ], ], 'NONE: No filter, user in no groups' => (object) [ 'loginuser' => 'd', 'groups' => [], 'jointype' => filter::JOINTYPE_NONE, 'count' => 0, 'expectedusers' => ['exception'], ], 'NONE: Filter on a single group, user in one group' => (object) [ 'loginuser' => 'a', 'groups' => ['groupa'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 0, 'expectedusers' => [], ], 'NONE: Filter on a single group, user in multple groups' => (object) [ 'loginuser' => 'c', 'groups' => ['groupa'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 1, 'expectedusers' => [ 'b', ], ], 'NONE: Filter on a single group, user in no groups' => (object) [ 'loginuser' => 'd', 'groups' => ['groupa'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 0, 'expectedusers' => ['exception'], ], 'NONE: Filter on multiple groups, user in one group (ignore invalid groups)' => (object) [ 'loginuser' => 'a', 'groups' => ['groupa', 'groupb'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 0, 'expectedusers' => [], ], 'NONE: Filter on multiple groups, user in multiple groups' => (object) [ 'loginuser' => 'c', 'groups' => ['groupa', 'groupb'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 0, 'expectedusers' => [], ], 'NONE: Filter on multiple groups or no groups, user in multiple groups (ignore no groups)' => (object) [ 'loginuser' => 'c', 'groups' => ['groupa', 'groupb', 'nogroups'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 0, 'expectedusers' => [], ], ], ], ]; $finaltests = []; foreach ($tests as $testname => $testdata) { foreach ($testdata->expect as $expectname => $expectdata) { $finaltests["{$testname} => {$expectname}"] = [ 'users' => $testdata->users, 'groupsavailable' => $testdata->groupsavailable, 'filtergroups' => $expectdata->groups, 'jointype' => $expectdata->jointype, 'count' => $expectdata->count, 'expectedusers' => $expectdata->expectedusers, 'loginusername' => $expectdata->loginuser, ]; } } return $finaltests; } /** * Ensure that the last access filter works as expected with the provided test cases. * * @param array $usersdata The list of users to create * @param array $accesssince The last access data to filter by * @param int $jointype The join type to use when combining filter values * @param int $count The expected count * @param array $expectedusers * @dataProvider accesssince_provider */ public function test_accesssince_filter(array $usersdata, array $accesssince, int $jointype, int $count, array $expectedusers): void { $course = $this->getDataGenerator()->create_course(); $coursecontext = context_course::instance($course->id); $users = []; foreach ($usersdata as $username => $userdata) { $usertimestamp = empty($userdata['lastlogin']) ? 0 : strtotime($userdata['lastlogin']); $user = $this->getDataGenerator()->create_user(['username' => $username]); $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student'); // Create the record of the user's last access to the course. if ($usertimestamp > 0) { $this->getDataGenerator()->create_user_course_lastaccess($user, $course, $usertimestamp); } $users[$username] = $user; } // Create a secondary course with users. We should not see these users. $this->create_course_with_users(1, 1, 1, 1); // Create the basic filter. $filterset = new participants_filterset(); $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id])); // Create the last access filter. $lastaccessfilter = new integer_filter('accesssince'); $filterset->add_filter($lastaccessfilter); // Configure the filter. foreach ($accesssince as $accessstring) { $lastaccessfilter->add_filter_value(strtotime($accessstring)); } $lastaccessfilter->set_join_type($jointype); // Run the search. $search = new participants_search($course, $coursecontext, $filterset); $rs = $search->get_participants(); $this->assertInstanceOf(moodle_recordset::class, $rs); $records = $this->convert_recordset_to_array($rs); $this->assertCount($count, $records); $this->assertEquals($count, $search->get_total_participants_count()); foreach ($expectedusers as $expecteduser) { $this->assertArrayHasKey($users[$expecteduser]->id, $records); } } /** * Data provider for last access filter tests. * * @return array */ public function accesssince_provider(): array { $tests = [ // Users with different last access times. 'Users in different groups' => (object) [ 'users' => [ 'a' => [ 'lastlogin' => '-3 days', ], 'b' => [ 'lastlogin' => '-2 weeks', ], 'c' => [ 'lastlogin' => '-5 months', ], 'd' => [ 'lastlogin' => '-11 months', ], 'e' => [ // Never logged in. 'lastlogin' => '', ], ], 'expect' => [ // Tests for jointype: ANY. 'ANY: No filter' => (object) [ 'accesssince' => [], 'jointype' => filter::JOINTYPE_ANY, 'count' => 5, 'expectedusers' => [ 'a', 'b', 'c', 'd', 'e', ], ], 'ANY: Filter on last login more than 1 year ago' => (object) [ 'accesssince' => ['-1 year'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 1, 'expectedusers' => [ 'e', ], ], 'ANY: Filter on last login more than 6 months ago' => (object) [ 'accesssince' => ['-6 months'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 2, 'expectedusers' => [ 'd', 'e', ], ], 'ANY: Filter on last login more than 3 weeks ago' => (object) [ 'accesssince' => ['-3 weeks'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 3, 'expectedusers' => [ 'c', 'd', 'e', ], ], 'ANY: Filter on last login more than 5 days ago' => (object) [ 'accesssince' => ['-5 days'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 4, 'expectedusers' => [ 'b', 'c', 'd', 'e', ], ], 'ANY: Filter on last login more than 2 days ago' => (object) [ 'accesssince' => ['-2 days'], 'jointype' => filter::JOINTYPE_ANY, 'count' => 5, 'expectedusers' => [ 'a', 'b', 'c', 'd', 'e', ], ], // Tests for jointype: ALL. 'ALL: No filter' => (object) [ 'accesssince' => [], 'jointype' => filter::JOINTYPE_ALL, 'count' => 5, 'expectedusers' => [ 'a', 'b', 'c', 'd', 'e', ], ], 'ALL: Filter on last login more than 1 year ago' => (object) [ 'accesssince' => ['-1 year'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 1, 'expectedusers' => [ 'e', ], ], 'ALL: Filter on last login more than 6 months ago' => (object) [ 'accesssince' => ['-6 months'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 2, 'expectedusers' => [ 'd', 'e', ], ], 'ALL: Filter on last login more than 3 weeks ago' => (object) [ 'accesssince' => ['-3 weeks'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 3, 'expectedusers' => [ 'c', 'd', 'e', ], ], 'ALL: Filter on last login more than 5 days ago' => (object) [ 'accesssince' => ['-5 days'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 4, 'expectedusers' => [ 'b', 'c', 'd', 'e', ], ], 'ALL: Filter on last login more than 2 days ago' => (object) [ 'accesssince' => ['-2 days'], 'jointype' => filter::JOINTYPE_ALL, 'count' => 5, 'expectedusers' => [ 'a', 'b', 'c', 'd', 'e', ], ], // Tests for jointype: NONE. 'NONE: No filter' => (object) [ 'accesssince' => [], 'jointype' => filter::JOINTYPE_NONE, 'count' => 5, 'expectedusers' => [ 'a', 'b', 'c', 'd', 'e', ], ], 'NONE: Filter on last login more than 1 year ago' => (object) [ 'accesssince' => ['-1 year'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 4, 'expectedusers' => [ 'a', 'b', 'c', 'd', ], ], 'NONE: Filter on last login more than 6 months ago' => (object) [ 'accesssince' => ['-6 months'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 3, 'expectedusers' => [ 'a', 'b', 'c', ], ], 'NONE: Filter on last login more than 3 weeks ago' => (object) [ 'accesssince' => ['-3 weeks'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 2, 'expectedusers' => [ 'a', 'b', ], ], 'NONE: Filter on last login more than 5 days ago' => (object) [ 'accesssince' => ['-5 days'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 1, 'expectedusers' => [ 'a', ], ], 'NONE: Filter on last login more than 2 days ago' => (object) [ 'accesssince' => ['-2 days'], 'jointype' => filter::JOINTYPE_NONE, 'count' => 0, 'expectedusers' => [], ], ], ], ]; $finaltests = []; foreach ($tests as $testname => $testdata) { foreach ($testdata->expect as $expectname => $expectdata) { $finaltests["{$testname} => {$expectname}"] = [ 'users' => $testdata->users, 'accesssince' => $expectdata->accesssince, 'jointype' => $expectdata->jointype, 'count' => $expectdata->count, 'expectedusers' => $expectdata->expectedusers, ]; } } return $finaltests; } /** * Ensure that the joins between filters in the filterset work as expected with the provided test cases. * * @param array $usersdata The list of users to create * @param array $filterdata The data to filter by * @param array $groupsavailable The names of groups that should be created in the course * @param int $jointype The join type to used between each filter being applied * @param int $count The expected count * @param array $expectedusers * @dataProvider filterset_joins_provider */ public function test_filterset_joins(array $usersdata, array $filterdata, array $groupsavailable, int $jointype, int $count, array $expectedusers): void { global $DB; // Ensure sufficient capabilities to view all statuses. $this->setAdminUser(); // Remove the default role. set_config('roleid', 0, 'enrol_manual'); $course = $this->getDataGenerator()->create_course(); $coursecontext = context_course::instance($course->id); $roles = $DB->get_records_menu('role', [], '', 'shortname, id'); $users = []; // Ensure all enrolment methods are enabled (and mapped where required for filtering later). $enrolinstances = enrol_get_instances($course->id, false); $enrolinstancesmap = []; foreach ($enrolinstances as $instance) { $plugin = enrol_get_plugin($instance->enrol); $plugin->update_status($instance, ENROL_INSTANCE_ENABLED); $enrolinstancesmap[$instance->enrol] = (int) $instance->id; } // Create the required course groups and mapping. $nogroupsdata = (object) [ 'id' => USERSWITHOUTGROUP, ]; $groupsdata = ['nogroups' => $nogroupsdata]; foreach ($groupsavailable as $groupname) { $groupinfo = [ 'courseid' => $course->id, 'name' => $groupname, ]; $groupsdata[$groupname] = $this->getDataGenerator()->create_group($groupinfo); } // Create test users. foreach ($usersdata as $username => $userdata) { $usertimestamp = empty($userdata['lastlogin']) ? 0 : strtotime($userdata['lastlogin']); unset($userdata['lastlogin']); // Prevent randomly generated field values that may cause false fails. $userdata['firstnamephonetic'] = $userdata['firstnamephonetic'] ?? $userdata['firstname']; $userdata['lastnamephonetic'] = $userdata['lastnamephonetic'] ?? $userdata['lastname']; $userdata['middlename'] = $userdata['middlename'] ?? ''; $userdata['alternatename'] = $userdata['alternatename'] ?? $username; $user = $this->getDataGenerator()->create_user($userdata); foreach ($userdata['enrolments'] as $details) { $this->getDataGenerator()->enrol_user($user->id, $course->id, $roles[$details['role']], $details['method'], 0, 0, $details['status']); } foreach ($userdata['groups'] as $groupname) { $userinfo = [ 'userid' => $user->id, 'groupid' => (int) $groupsdata[$groupname]->id, ]; $this->getDataGenerator()->create_group_member($userinfo); } if ($usertimestamp > 0) { $this->getDataGenerator()->create_user_course_lastaccess($user, $course, $usertimestamp); } $users[$username] = $user; } // Create a secondary course with users. We should not see these users. $this->create_course_with_users(10, 10, 10, 10); // Create the basic filterset. $filterset = new participants_filterset(); $filterset->set_join_type($jointype); $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id])); // Apply the keywords filter if required. if (array_key_exists('keywords', $filterdata)) { $keywordfilter = new string_filter('keywords'); $filterset->add_filter($keywordfilter); foreach ($filterdata['keywords']['values'] as $keyword) { $keywordfilter->add_filter_value($keyword); } $keywordfilter->set_join_type($filterdata['keywords']['jointype']); } // Apply enrolment methods filter if required. if (array_key_exists('enrolmethods', $filterdata)) { $enrolmethodfilter = new integer_filter('enrolments'); $filterset->add_filter($enrolmethodfilter); foreach ($filterdata['enrolmethods']['values'] as $enrolmethod) { $enrolmethodfilter->add_filter_value($enrolinstancesmap[$enrolmethod]); } $enrolmethodfilter->set_join_type($filterdata['enrolmethods']['jointype']); } // Apply roles filter if required. if (array_key_exists('courseroles', $filterdata)) { $rolefilter = new integer_filter('roles'); $filterset->add_filter($rolefilter); foreach ($filterdata['courseroles']['values'] as $rolename) { $rolefilter->add_filter_value((int) $roles[$rolename]); } $rolefilter->set_join_type($filterdata['courseroles']['jointype']); } // Apply status filter if required. if (array_key_exists('status', $filterdata)) { $statusfilter = new integer_filter('status'); $filterset->add_filter($statusfilter); foreach ($filterdata['status']['values'] as $status) { $statusfilter->add_filter_value($status); } $statusfilter->set_join_type($filterdata['status']['jointype']); } // Apply groups filter if required. if (array_key_exists('groups', $filterdata)) { $groupsfilter = new integer_filter('groups'); $filterset->add_filter($groupsfilter); foreach ($filterdata['groups']['values'] as $filtergroupname) { $groupsfilter->add_filter_value((int) $groupsdata[$filtergroupname]->id); } $groupsfilter->set_join_type($filterdata['groups']['jointype']); } // Apply last access filter if required. if (array_key_exists('accesssince', $filterdata)) { $lastaccessfilter = new integer_filter('accesssince'); $filterset->add_filter($lastaccessfilter); foreach ($filterdata['accesssince']['values'] as $accessstring) { $lastaccessfilter->add_filter_value(strtotime($accessstring)); } $lastaccessfilter->set_join_type($filterdata['accesssince']['jointype']); } // Run the search. $search = new participants_search($course, $coursecontext, $filterset); $rs = $search->get_participants(); $this->assertInstanceOf(moodle_recordset::class, $rs); $records = $this->convert_recordset_to_array($rs); $this->assertCount($count, $records); $this->assertEquals($count, $search->get_total_participants_count()); foreach ($expectedusers as $expecteduser) { $this->assertArrayHasKey($users[$expecteduser]->id, $records); } } /** * Data provider for filterset join tests. * * @return array */ public function filterset_joins_provider(): array { $tests = [ // Users with different configurations. 'Users with different configurations' => (object) [ 'groupsavailable' => [ 'groupa', 'groupb', 'groupc', ], 'users' => [ 'adam.ant' => [ 'firstname' => 'Adam', 'lastname' => 'Ant', 'enrolments' => [ [ 'role' => 'student', 'method' => 'manual', 'status' => ENROL_USER_ACTIVE, ], ], 'groups' => ['groupa'], 'lastlogin' => '-3 days', ], 'barbara.bennett' => [ 'firstname' => 'Barbara', 'lastname' => 'Bennett', 'enrolments' => [ [ 'role' => 'student', 'method' => 'manual', 'status' => ENROL_USER_ACTIVE, ], [ 'role' => 'teacher', 'method' => 'manual', 'status' => ENROL_USER_ACTIVE, ], ], 'groups' => ['groupb'], 'lastlogin' => '-2 weeks', ], 'colin.carnforth' => [ 'firstname' => 'Colin', 'lastname' => 'Carnforth', 'enrolments' => [ [ 'role' => 'editingteacher', 'method' => 'self', 'status' => ENROL_USER_SUSPENDED, ], ], 'groups' => ['groupa', 'groupb'], 'lastlogin' => '-5 months', ], 'tony.rogers' => [ 'firstname' => 'Anthony', 'lastname' => 'Rogers', 'enrolments' => [ [ 'role' => 'editingteacher', 'method' => 'self', 'status' => ENROL_USER_SUSPENDED, ], ], 'groups' => [], 'lastlogin' => '-10 months', ], 'sarah.rester' => [ 'firstname' => 'Sarah', 'lastname' => 'Rester', 'email' => 'zazu@example.com', 'enrolments' => [ [ 'role' => 'teacher', 'method' => 'manual', 'status' => ENROL_USER_ACTIVE, ], [ 'role' => 'editingteacher', 'method' => 'self', 'status' => ENROL_USER_SUSPENDED, ], ], 'groups' => [], 'lastlogin' => '-11 months', ], 'morgan.crikeyson' => [ 'firstname' => 'Morgan', 'lastname' => 'Crikeyson', 'enrolments' => [ [ 'role' => 'teacher', 'method' => 'manual', 'status' => ENROL_USER_ACTIVE, ], ], 'groups' => ['groupa'], 'lastlogin' => '-1 week', ], 'jonathan.bravo' => [ 'firstname' => 'Jonathan', 'lastname' => 'Bravo', 'enrolments' => [ [ 'role' => 'student', 'method' => 'manual', 'status' => ENROL_USER_ACTIVE, ], ], 'groups' => [], // Never logged in. 'lastlogin' => '', ], ], 'expect' => [ // Tests for jointype: ANY. 'ANY: No filters in filterset' => (object) [ 'filterdata' => [], 'jointype' => filter::JOINTYPE_ANY, 'count' => 7, 'expectedusers' => [ 'adam.ant', 'barbara.bennett', 'colin.carnforth', 'tony.rogers', 'sarah.rester', 'morgan.crikeyson', 'jonathan.bravo', ], ], 'ANY: Filterset containing a single filter type' => (object) [ 'filterdata' => [ 'enrolmethods' => [ 'values' => ['self'], 'jointype' => filter::JOINTYPE_ANY, ], ], 'jointype' => filter::JOINTYPE_ANY, 'count' => 3, 'expectedusers' => [ 'colin.carnforth', 'tony.rogers', 'sarah.rester', ], ], 'ANY: Filterset matching all filter types on different users' => (object) [ 'filterdata' => [ // Match Adam only. 'keywords' => [ 'values' => ['adam'], 'jointype' => filter::JOINTYPE_ALL, ], // Match Sarah only. 'enrolmethods' => [ 'values' => ['manual', 'self'], 'jointype' => filter::JOINTYPE_ALL, ], // Match Barbara only. 'courseroles' => [ 'values' => ['student', 'teacher'], 'jointype' => filter::JOINTYPE_ALL, ], // Match Sarah only. 'status' => [ 'values' => [ENROL_USER_ACTIVE, ENROL_USER_SUSPENDED], 'jointype' => filter::JOINTYPE_ALL, ], // Match Colin only. 'groups' => [ 'values' => ['groupa', 'groupb'], 'jointype' => filter::JOINTYPE_ALL, ], // Match Jonathan only. 'accesssince' => [ 'values' => ['-1 year'], 'jointype' => filter::JOINTYPE_ALL, ], ], 'jointype' => filter::JOINTYPE_ANY, 'count' => 5, // Morgan and Tony are not matched, to confirm filtering is not just returning all users. 'expectedusers' => [ 'adam.ant', 'barbara.bennett', 'colin.carnforth', 'sarah.rester', 'jonathan.bravo', ], ], // Tests for jointype: ALL. 'ALL: No filters in filterset' => (object) [ 'filterdata' => [], 'jointype' => filter::JOINTYPE_ALL, 'count' => 7, 'expectedusers' => [ 'adam.ant', 'barbara.bennett', 'colin.carnforth', 'tony.rogers', 'sarah.rester', 'morgan.crikeyson', 'jonathan.bravo', ], ], 'ALL: Filterset containing a single filter type' => (object) [ 'filterdata' => [ 'enrolmethods' => [ 'values' => ['self'], 'jointype' => filter::JOINTYPE_ANY, ], ], 'jointype' => filter::JOINTYPE_ALL, 'count' => 3, 'expectedusers' => [ 'colin.carnforth', 'tony.rogers', 'sarah.rester', ], ], 'ALL: Filterset combining all filter types' => (object) [ 'filterdata' => [ // Exclude Adam, Tony, Morgan and Jonathan. 'keywords' => [ 'values' => ['ar'], 'jointype' => filter::JOINTYPE_ANY, ], // Exclude Colin and Tony. 'enrolmethods' => [ 'values' => ['manual'], 'jointype' => filter::JOINTYPE_ANY, ], // Exclude Adam, Barbara and Jonathan. 'courseroles' => [ 'values' => ['student'], 'jointype' => filter::JOINTYPE_NONE, ], // Exclude Colin and Tony. 'status' => [ 'values' => [ENROL_USER_ACTIVE], 'jointype' => filter::JOINTYPE_ALL, ], // Exclude Barbara. 'groups' => [ 'values' => ['groupa', 'nogroups'], 'jointype' => filter::JOINTYPE_ANY, ], // Exclude Adam, Colin and Barbara. 'accesssince' => [ 'values' => ['-6 months'], 'jointype' => filter::JOINTYPE_ALL, ], ], 'jointype' => filter::JOINTYPE_ALL, 'count' => 1, 'expectedusers' => [ 'sarah.rester', ], ], // Tests for jointype: NONE. 'NONE: No filters in filterset' => (object) [ 'filterdata' => [], 'jointype' => filter::JOINTYPE_NONE, 'count' => 7, 'expectedusers' => [ 'adam.ant', 'barbara.bennett', 'colin.carnforth', 'tony.rogers', 'sarah.rester', 'morgan.crikeyson', 'jonathan.bravo', ], ], 'NONE: Filterset containing a single filter type' => (object) [ 'filterdata' => [ 'enrolmethods' => [ 'values' => ['self'], 'jointype' => filter::JOINTYPE_ANY, ], ], 'jointype' => filter::JOINTYPE_NONE, 'count' => 4, 'expectedusers' => [ 'adam.ant', 'barbara.bennett', 'morgan.crikeyson', 'jonathan.bravo', ], ], 'NONE: Filterset combining all filter types' => (object) [ 'filterdata' => [ // Excludes Adam. 'keywords' => [ 'values' => ['adam'], 'jointype' => filter::JOINTYPE_ANY, ], // Excludes Colin, Tony and Sarah. 'enrolmethods' => [ 'values' => ['self'], 'jointype' => filter::JOINTYPE_ANY, ], // Excludes Jonathan. 'courseroles' => [ 'values' => ['student'], 'jointype' => filter::JOINTYPE_NONE, ], // Excludes Colin, Tony and Sarah. 'status' => [ 'values' => [ENROL_USER_SUSPENDED], 'jointype' => filter::JOINTYPE_ALL, ], // Excludes Adam, Colin, Tony, Sarah, Morgan and Jonathan. 'groups' => [ 'values' => ['groupa', 'nogroups'], 'jointype' => filter::JOINTYPE_ANY, ], // Excludes Tony and Sarah. 'accesssince' => [ 'values' => ['-6 months'], 'jointype' => filter::JOINTYPE_ALL, ], ], 'jointype' => filter::JOINTYPE_NONE, 'count' => 1, 'expectedusers' => [ 'barbara.bennett', ], ], 'NONE: Filterset combining several filter types and a double-negative on keyword' => (object) [ 'jointype' => filter::JOINTYPE_NONE, 'filterdata' => [ // Note: This is a jointype NONE on the parent jointype NONE. // The result therefore negated in this instance. // Include Adam and Anthony. 'keywords' => [ 'values' => ['ant'], 'jointype' => filter::JOINTYPE_NONE, ], // Excludes Tony. 'status' => [ 'values' => [ENROL_USER_SUSPENDED], 'jointype' => filter::JOINTYPE_ALL, ], ], 'count' => 1, 'expectedusers' => [ 'adam.ant', ], ], ], ], ]; $finaltests = []; foreach ($tests as $testname => $testdata) { foreach ($testdata->expect as $expectname => $expectdata) { $finaltests["{$testname} => {$expectname}"] = [ 'users' => $testdata->users, 'filterdata' => $expectdata->filterdata, 'groupsavailable' => $testdata->groupsavailable, 'jointype' => $expectdata->jointype, 'count' => $expectdata->count, 'expectedusers' => $expectdata->expectedusers, ]; } } return $finaltests; } }