Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401]

   1  <?php
   2  // This file is part of Moodle - https://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Provides {@link core_user_table_participants_search_test} class.
  19   *
  20   * @package   core_user
  21   * @category  test
  22   * @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
  23   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  declare(strict_types=1);
  27  
  28  namespace core_user\table;
  29  
  30  use advanced_testcase;
  31  use context_course;
  32  use context_coursecat;
  33  use core_table\local\filter\filter;
  34  use core_table\local\filter\integer_filter;
  35  use core_table\local\filter\string_filter;
  36  use core_user\table\participants_filterset;
  37  use core_user\table\participants_search;
  38  use moodle_recordset;
  39  use stdClass;
  40  
  41  /**
  42   * Tests for the implementation of {@link core_user_table_participants_search} class.
  43   *
  44   * @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
  45   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  46   */
  47  class participants_search_test extends advanced_testcase {
  48  
  49      /**
  50       * Helper to convert a moodle_recordset to an array of records.
  51       *
  52       * @param moodle_recordset $recordset
  53       * @return array
  54       */
  55      protected function convert_recordset_to_array(moodle_recordset $recordset): array {
  56          $records = [];
  57          foreach ($recordset as $record) {
  58              $records[$record->id] = $record;
  59          }
  60          $recordset->close();
  61  
  62          return $records;
  63      }
  64  
  65      /**
  66       * Create and enrol a set of users into the specified course.
  67       *
  68       * @param stdClass $course
  69       * @param int $count
  70       * @param null|string $role
  71       * @return array
  72       */
  73      protected function create_and_enrol_users(stdClass $course, int $count, ?string $role = null): array {
  74          $this->resetAfterTest(true);
  75          $users = [];
  76  
  77          for ($i = 0; $i < $count; $i++) {
  78              $user = $this->getDataGenerator()->create_user();
  79              $this->getDataGenerator()->enrol_user($user->id, $course->id, $role);
  80              $users[] = $user;
  81          }
  82  
  83          return $users;
  84      }
  85  
  86      /**
  87       * Create a new course with several types of user.
  88       *
  89       * @param int $editingteachers The number of editing teachers to create in the course.
  90       * @param int $teachers The number of non-editing teachers to create in the course.
  91       * @param int $students The number of students to create in the course.
  92       * @param int $norole The number of users with no role to create in the course.
  93       * @return stdClass
  94       */
  95      protected function create_course_with_users(int $editingteachers, int $teachers, int $students, int $norole): stdClass {
  96          $data = (object) [
  97              'course' => $this->getDataGenerator()->create_course(),
  98              'editingteachers' => [],
  99              'teachers' => [],
 100              'students' => [],
 101              'norole' => [],
 102          ];
 103  
 104          $data->context = context_course::instance($data->course->id);
 105  
 106          $data->editingteachers = $this->create_and_enrol_users($data->course, $editingteachers, 'editingteacher');
 107          $data->teachers = $this->create_and_enrol_users($data->course, $teachers, 'teacher');
 108          $data->students = $this->create_and_enrol_users($data->course, $students, 'student');
 109          $data->norole = $this->create_and_enrol_users($data->course, $norole);
 110  
 111          return $data;
 112      }
 113      /**
 114       * Ensure that the roles filter works as expected with the provided test cases.
 115       *
 116       * @param array $usersdata The list of users and their roles to create
 117       * @param array $testroles The list of roles to filter by
 118       * @param int $jointype The join type to use when combining filter values
 119       * @param int $count The expected count
 120       * @param array $expectedusers
 121       * @dataProvider role_provider
 122       */
 123      public function test_roles_filter(array $usersdata, array $testroles, int $jointype, int $count, array $expectedusers): void {
 124          global $DB;
 125  
 126          $roles = $DB->get_records_menu('role', [], '', 'shortname, id');
 127  
 128          // Remove the default role.
 129          set_config('roleid', 0, 'enrol_manual');
 130  
 131          $course = $this->getDataGenerator()->create_course();
 132          $coursecontext = context_course::instance($course->id);
 133  
 134          $category = $DB->get_record('course_categories', ['id' => $course->category]);
 135          $categorycontext = context_coursecat::instance($category->id);
 136  
 137          $users = [];
 138  
 139          foreach ($usersdata as $username => $userdata) {
 140              $user = $this->getDataGenerator()->create_user(['username' => $username]);
 141  
 142              if (array_key_exists('courseroles', $userdata)) {
 143                  $this->getDataGenerator()->enrol_user($user->id, $course->id, null);
 144                  foreach ($userdata['courseroles'] as $rolename) {
 145                      $this->getDataGenerator()->role_assign($roles[$rolename], $user->id, $coursecontext->id);
 146                  }
 147              }
 148  
 149              if (array_key_exists('categoryroles', $userdata)) {
 150                  foreach ($userdata['categoryroles'] as $rolename) {
 151                      $this->getDataGenerator()->role_assign($roles[$rolename], $user->id, $categorycontext->id);
 152                  }
 153              }
 154              $users[$username] = $user;
 155          }
 156  
 157          // Create a secondary course with users. We should not see these users.
 158          $this->create_course_with_users(1, 1, 1, 1);
 159  
 160          // Create the basic filter.
 161          $filterset = new participants_filterset();
 162          $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id]));
 163  
 164          // Create the role filter.
 165          $rolefilter = new integer_filter('roles');
 166          $filterset->add_filter($rolefilter);
 167  
 168          // Configure the filter.
 169          foreach ($testroles as $rolename) {
 170              $rolefilter->add_filter_value((int) $roles[$rolename]);
 171          }
 172          $rolefilter->set_join_type($jointype);
 173  
 174          // Run the search.
 175          $search = new participants_search($course, $coursecontext, $filterset);
 176          $rs = $search->get_participants();
 177          $this->assertInstanceOf(moodle_recordset::class, $rs);
 178          $records = $this->convert_recordset_to_array($rs);
 179  
 180          $this->assertCount($count, $records);
 181          $this->assertEquals($count, $search->get_total_participants_count());
 182  
 183          foreach ($expectedusers as $expecteduser) {
 184              $this->assertArrayHasKey($users[$expecteduser]->id, $records);
 185          }
 186      }
 187  
 188      /**
 189       * Data provider for role tests.
 190       *
 191       * @return array
 192       */
 193      public function role_provider(): array {
 194          $tests = [
 195              // Users who only have one role each.
 196              'Users in each role' => (object) [
 197                  'users' => [
 198                      'a' => [
 199                          'courseroles' => [
 200                              'student',
 201                          ],
 202                      ],
 203                      'b' => [
 204                          'courseroles' => [
 205                              'student',
 206                          ],
 207                      ],
 208                      'c' => [
 209                          'courseroles' => [
 210                              'editingteacher',
 211                          ],
 212                      ],
 213                      'd' => [
 214                          'courseroles' => [
 215                              'editingteacher',
 216                          ],
 217                      ],
 218                      'e' => [
 219                          'courseroles' => [
 220                              'teacher',
 221                          ],
 222                      ],
 223                      'f' => [
 224                          'courseroles' => [
 225                              'teacher',
 226                          ],
 227                      ],
 228                      // User is enrolled in the course without role.
 229                      'g' => [
 230                          'courseroles' => [
 231                          ],
 232                      ],
 233  
 234                      // User is a category manager and also enrolled without role in the course.
 235                      'h' => [
 236                          'courseroles' => [
 237                          ],
 238                          'categoryroles' => [
 239                              'manager',
 240                          ],
 241                      ],
 242  
 243                      // User is a category manager and not enrolled in the course.
 244                      // This user should not show up in any filter.
 245                      'i' => [
 246                          'categoryroles' => [
 247                              'manager',
 248                          ],
 249                      ],
 250                  ],
 251                  'expect' => [
 252                      // Tests for jointype: ANY.
 253                      'ANY: No role filter' => (object) [
 254                          'roles' => [],
 255                          'jointype' => filter::JOINTYPE_ANY,
 256                          'count' => 8,
 257                          'expectedusers' => [
 258                              'a',
 259                              'b',
 260                              'c',
 261                              'd',
 262                              'e',
 263                              'f',
 264                              'g',
 265                              'h',
 266                          ],
 267                      ],
 268                      'ANY: Filter on student' => (object) [
 269                          'roles' => ['student'],
 270                          'jointype' => filter::JOINTYPE_ANY,
 271                          'count' => 2,
 272                          'expectedusers' => [
 273                              'a',
 274                              'b',
 275                          ],
 276                      ],
 277                      'ANY: Filter on student, teacher' => (object) [
 278                          'roles' => ['student', 'teacher'],
 279                          'jointype' => filter::JOINTYPE_ANY,
 280                          'count' => 4,
 281                          'expectedusers' => [
 282                              'a',
 283                              'b',
 284                              'e',
 285                              'f',
 286                          ],
 287                      ],
 288                      'ANY: Filter on student, manager (category level role)' => (object) [
 289                          'roles' => ['student', 'manager'],
 290                          'jointype' => filter::JOINTYPE_ANY,
 291                          'count' => 3,
 292                          'expectedusers' => [
 293                              'a',
 294                              'b',
 295                              'h',
 296                          ],
 297                      ],
 298                      'ANY: Filter on student, coursecreator (not assigned)' => (object) [
 299                          'roles' => ['student', 'coursecreator'],
 300                          'jointype' => filter::JOINTYPE_ANY,
 301                          'count' => 2,
 302                          'expectedusers' => [
 303                              'a',
 304                              'b',
 305                          ],
 306                      ],
 307  
 308                      // Tests for jointype: ALL.
 309                      'ALL: No role filter' => (object) [
 310                          'roles' => [],
 311                          'jointype' => filter::JOINTYPE_ALL,
 312                          'count' => 8,
 313                          'expectedusers' => [
 314                              'a',
 315                              'b',
 316                              'c',
 317                              'd',
 318                              'e',
 319                              'f',
 320                              'g',
 321                              'h',
 322                          ],
 323                      ],
 324                      'ALL: Filter on student' => (object) [
 325                          'roles' => ['student'],
 326                          'jointype' => filter::JOINTYPE_ALL,
 327                          'count' => 2,
 328                          'expectedusers' => [
 329                              'a',
 330                              'b',
 331                          ],
 332                      ],
 333                      'ALL: Filter on student, teacher' => (object) [
 334                          'roles' => ['student', 'teacher'],
 335                          'jointype' => filter::JOINTYPE_ALL,
 336                          'count' => 0,
 337                          'expectedusers' => [],
 338                      ],
 339                      'ALL: Filter on student, manager (category level role))' => (object) [
 340                          'roles' => ['student', 'manager'],
 341                          'jointype' => filter::JOINTYPE_ALL,
 342                          'count' => 0,
 343                          'expectedusers' => [],
 344                      ],
 345                      'ALL: Filter on student, coursecreator (not assigned))' => (object) [
 346                          'roles' => ['student', 'coursecreator'],
 347                          'jointype' => filter::JOINTYPE_ALL,
 348                          'count' => 0,
 349                          'expectedusers' => [],
 350                      ],
 351  
 352                      // Tests for jointype: NONE.
 353                      'NONE: No role filter' => (object) [
 354                          'roles' => [],
 355                          'jointype' => filter::JOINTYPE_NONE,
 356                          'count' => 8,
 357                          'expectedusers' => [
 358                              'a',
 359                              'b',
 360                              'c',
 361                              'd',
 362                              'e',
 363                              'f',
 364                              'g',
 365                              'h',
 366                          ],
 367                      ],
 368                      'NONE: Filter on student' => (object) [
 369                          'roles' => ['student'],
 370                          'jointype' => filter::JOINTYPE_NONE,
 371                          'count' => 6,
 372                          'expectedusers' => [
 373                              'c',
 374                              'd',
 375                              'e',
 376                              'f',
 377                              'g',
 378                              'h',
 379                          ],
 380                      ],
 381                      'NONE: Filter on student, teacher' => (object) [
 382                          'roles' => ['student', 'teacher'],
 383                          'jointype' => filter::JOINTYPE_NONE,
 384                          'count' => 4,
 385                          'expectedusers' => [
 386                              'c',
 387                              'd',
 388                              'g',
 389                              'h',
 390                          ],
 391                      ],
 392                      'NONE: Filter on student, manager (category level role))' => (object) [
 393                          'roles' => ['student', 'manager'],
 394                          'jointype' => filter::JOINTYPE_NONE,
 395                          'count' => 5,
 396                          'expectedusers' => [
 397                              'c',
 398                              'd',
 399                              'e',
 400                              'f',
 401                              'g',
 402                          ],
 403                      ],
 404                      'NONE: Filter on student, coursecreator (not assigned))' => (object) [
 405                          'roles' => ['student', 'coursecreator'],
 406                          'jointype' => filter::JOINTYPE_NONE,
 407                          'count' => 6,
 408                          'expectedusers' => [
 409                              'c',
 410                              'd',
 411                              'e',
 412                              'f',
 413                              'g',
 414                              'h',
 415                          ],
 416                      ],
 417                  ],
 418              ],
 419              'Users with multiple roles' => (object) [
 420                  'users' => [
 421                      'a' => [
 422                          'courseroles' => [
 423                              'student',
 424                          ],
 425                      ],
 426                      'b' => [
 427                          'courseroles' => [
 428                              'student',
 429                              'teacher',
 430                          ],
 431                      ],
 432                      'c' => [
 433                          'courseroles' => [
 434                              'editingteacher',
 435                          ],
 436                      ],
 437                      'd' => [
 438                          'courseroles' => [
 439                              'editingteacher',
 440                          ],
 441                      ],
 442                      'e' => [
 443                          'courseroles' => [
 444                              'teacher',
 445                              'editingteacher',
 446                          ],
 447                      ],
 448                      'f' => [
 449                          'courseroles' => [
 450                              'teacher',
 451                          ],
 452                      ],
 453  
 454                      // User is enrolled in the course without role.
 455                      'g' => [
 456                          'courseroles' => [
 457                          ],
 458                      ],
 459  
 460                      // User is a category manager and also enrolled without role in the course.
 461                      'h' => [
 462                          'courseroles' => [
 463                          ],
 464                          'categoryroles' => [
 465                              'manager',
 466                          ],
 467                      ],
 468  
 469                      // User is a category manager and not enrolled in the course.
 470                      // This user should not show up in any filter.
 471                      'i' => [
 472                          'categoryroles' => [
 473                              'manager',
 474                          ],
 475                      ],
 476                  ],
 477                  'expect' => [
 478                      // Tests for jointype: ANY.
 479                      'ANY: No role filter' => (object) [
 480                          'roles' => [],
 481                          'jointype' => filter::JOINTYPE_ANY,
 482                          'count' => 8,
 483                          'expectedusers' => [
 484                              'a',
 485                              'b',
 486                              'c',
 487                              'd',
 488                              'e',
 489                              'f',
 490                              'g',
 491                              'h',
 492                          ],
 493                      ],
 494                      'ANY: Filter on student' => (object) [
 495                          'roles' => ['student'],
 496                          'jointype' => filter::JOINTYPE_ANY,
 497                          'count' => 2,
 498                          'expectedusers' => [
 499                              'a',
 500                              'b',
 501                          ],
 502                      ],
 503                      'ANY: Filter on teacher' => (object) [
 504                          'roles' => ['teacher'],
 505                          'jointype' => filter::JOINTYPE_ANY,
 506                          'count' => 3,
 507                          'expectedusers' => [
 508                              'b',
 509                              'e',
 510                              'f',
 511                          ],
 512                      ],
 513                      'ANY: Filter on editingteacher' => (object) [
 514                          'roles' => ['editingteacher'],
 515                          'jointype' => filter::JOINTYPE_ANY,
 516                          'count' => 3,
 517                          'expectedusers' => [
 518                              'c',
 519                              'd',
 520                              'e',
 521                          ],
 522                      ],
 523                      'ANY: Filter on student, teacher' => (object) [
 524                          'roles' => ['student', 'teacher'],
 525                          'jointype' => filter::JOINTYPE_ANY,
 526                          'count' => 4,
 527                          'expectedusers' => [
 528                              'a',
 529                              'b',
 530                              'e',
 531                              'f',
 532                          ],
 533                      ],
 534                      'ANY: Filter on teacher, editingteacher' => (object) [
 535                          'roles' => ['teacher', 'editingteacher'],
 536                          'jointype' => filter::JOINTYPE_ANY,
 537                          'count' => 5,
 538                          'expectedusers' => [
 539                              'b',
 540                              'c',
 541                              'd',
 542                              'e',
 543                              'f',
 544                          ],
 545                      ],
 546                      'ANY: Filter on student, manager (category level role)' => (object) [
 547                          'roles' => ['student', 'manager'],
 548                          'jointype' => filter::JOINTYPE_ANY,
 549                          'count' => 3,
 550                          'expectedusers' => [
 551                              'a',
 552                              'b',
 553                              'h',
 554                          ],
 555                      ],
 556                      'ANY: Filter on student, coursecreator (not assigned)' => (object) [
 557                          'roles' => ['student', 'coursecreator'],
 558                          'jointype' => filter::JOINTYPE_ANY,
 559                          'count' => 2,
 560                          'expectedusers' => [
 561                              'a',
 562                              'b',
 563                          ],
 564                      ],
 565  
 566                      // Tests for jointype: ALL.
 567                      'ALL: No role filter' => (object) [
 568                          'roles' => [],
 569                          'jointype' => filter::JOINTYPE_ALL,
 570                          'count' => 8,
 571                          'expectedusers' => [
 572                              'a',
 573                              'b',
 574                              'c',
 575                              'd',
 576                              'e',
 577                              'f',
 578                              'g',
 579                              'h',
 580                          ],
 581                      ],
 582                      'ALL: Filter on student' => (object) [
 583                          'roles' => ['student'],
 584                          'jointype' => filter::JOINTYPE_ALL,
 585                          'count' => 2,
 586                          'expectedusers' => [
 587                              'a',
 588                              'b',
 589                          ],
 590                      ],
 591                      'ALL: Filter on teacher' => (object) [
 592                          'roles' => ['teacher'],
 593                          'jointype' => filter::JOINTYPE_ALL,
 594                          'count' => 3,
 595                          'expectedusers' => [
 596                              'b',
 597                              'e',
 598                              'f',
 599                          ],
 600                      ],
 601                      'ALL: Filter on editingteacher' => (object) [
 602                          'roles' => ['editingteacher'],
 603                          'jointype' => filter::JOINTYPE_ALL,
 604                          'count' => 3,
 605                          'expectedusers' => [
 606                              'c',
 607                              'd',
 608                              'e',
 609                          ],
 610                      ],
 611                      'ALL: Filter on student, teacher' => (object) [
 612                          'roles' => ['student', 'teacher'],
 613                          'jointype' => filter::JOINTYPE_ALL,
 614                          'count' => 1,
 615                          'expectedusers' => [
 616                              'b',
 617                          ],
 618                      ],
 619                      'ALL: Filter on teacher, editingteacher' => (object) [
 620                          'roles' => ['teacher', 'editingteacher'],
 621                          'jointype' => filter::JOINTYPE_ALL,
 622                          'count' => 1,
 623                          'expectedusers' => [
 624                              'e',
 625                          ],
 626                      ],
 627                      'ALL: Filter on student, manager (category level role)' => (object) [
 628                          'roles' => ['student', 'manager'],
 629                          'jointype' => filter::JOINTYPE_ALL,
 630                          'count' => 0,
 631                          'expectedusers' => [],
 632                      ],
 633                      'ALL: Filter on student, coursecreator (not assigned)' => (object) [
 634                          'roles' => ['student', 'coursecreator'],
 635                          'jointype' => filter::JOINTYPE_ALL,
 636                          'count' => 0,
 637                          'expectedusers' => [],
 638                      ],
 639  
 640                      // Tests for jointype: NONE.
 641                      'NONE: No role filter' => (object) [
 642                          'roles' => [],
 643                          'jointype' => filter::JOINTYPE_NONE,
 644                          'count' => 8,
 645                          'expectedusers' => [
 646                              'a',
 647                              'b',
 648                              'c',
 649                              'd',
 650                              'e',
 651                              'f',
 652                              'g',
 653                              'h',
 654                          ],
 655                      ],
 656                      'NONE: Filter on student' => (object) [
 657                          'roles' => ['student'],
 658                          'jointype' => filter::JOINTYPE_NONE,
 659                          'count' => 6,
 660                          'expectedusers' => [
 661                              'c',
 662                              'd',
 663                              'e',
 664                              'f',
 665                              'g',
 666                              'h',
 667                          ],
 668                      ],
 669                      'NONE: Filter on teacher' => (object) [
 670                          'roles' => ['teacher'],
 671                          'jointype' => filter::JOINTYPE_NONE,
 672                          'count' => 5,
 673                          'expectedusers' => [
 674                              'a',
 675                              'c',
 676                              'd',
 677                              'g',
 678                              'h',
 679                          ],
 680                      ],
 681                      'NONE: Filter on editingteacher' => (object) [
 682                          'roles' => ['editingteacher'],
 683                          'jointype' => filter::JOINTYPE_NONE,
 684                          'count' => 5,
 685                          'expectedusers' => [
 686                              'a',
 687                              'b',
 688                              'f',
 689                              'g',
 690                              'h',
 691                          ],
 692                      ],
 693                      'NONE: Filter on student, teacher' => (object) [
 694                          'roles' => ['student', 'teacher'],
 695                          'jointype' => filter::JOINTYPE_NONE,
 696                          'count' => 5,
 697                          'expectedusers' => [
 698                              'c',
 699                              'd',
 700                              'e',
 701                              'g',
 702                              'h',
 703                          ],
 704                      ],
 705                      'NONE: Filter on student, teacher' => (object) [
 706                          'roles' => ['teacher', 'editingteacher'],
 707                          'jointype' => filter::JOINTYPE_NONE,
 708                          'count' => 3,
 709                          'expectedusers' => [
 710                              'a',
 711                              'g',
 712                              'h',
 713                          ],
 714                      ],
 715                      'NONE: Filter on student, manager (category level role)' => (object) [
 716                          'roles' => ['student', 'manager'],
 717                          'jointype' => filter::JOINTYPE_NONE,
 718                          'count' => 5,
 719                          'expectedusers' => [
 720                              'c',
 721                              'd',
 722                              'e',
 723                              'f',
 724                              'g',
 725                          ],
 726                      ],
 727                      'NONE: Filter on student, coursecreator (not assigned)' => (object) [
 728                          'roles' => ['student', 'coursecreator'],
 729                          'jointype' => filter::JOINTYPE_NONE,
 730                          'count' => 6,
 731                          'expectedusers' => [
 732                              'c',
 733                              'd',
 734                              'e',
 735                              'f',
 736                              'g',
 737                              'h',
 738                          ],
 739                      ],
 740                  ],
 741              ],
 742          ];
 743  
 744          $finaltests = [];
 745          foreach ($tests as $testname => $testdata) {
 746              foreach ($testdata->expect as $expectname => $expectdata) {
 747                  $finaltests["{$testname} => {$expectname}"] = [
 748                      'users' => $testdata->users,
 749                      'roles' => $expectdata->roles,
 750                      'jointype' => $expectdata->jointype,
 751                      'count' => $expectdata->count,
 752                      'expectedusers' => $expectdata->expectedusers,
 753                  ];
 754              }
 755          }
 756  
 757          return $finaltests;
 758      }
 759  
 760      /**
 761       * Test participant search country filter
 762       *
 763       * @param array $usersdata
 764       * @param array $countries
 765       * @param int $jointype
 766       * @param array $expectedusers
 767       *
 768       * @dataProvider country_provider
 769       */
 770      public function test_country_filter(array $usersdata, array $countries, int $jointype, array $expectedusers): void {
 771          $this->resetAfterTest();
 772  
 773          $course = $this->getDataGenerator()->create_course();
 774          $users = [];
 775  
 776          foreach ($usersdata as $username => $country) {
 777              $users[$username] = $this->getDataGenerator()->create_and_enrol($course, 'student', (object) [
 778                  'username' => $username,
 779                  'country' => $country,
 780              ]);
 781          }
 782  
 783          // Add filters (courseid is required).
 784          $filterset = new participants_filterset();
 785          $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id]));
 786          $filterset->add_filter(new string_filter('country', $jointype, $countries));
 787  
 788          // Run the search, assert count matches the number of expected users.
 789          $search = new participants_search($course, context_course::instance($course->id), $filterset);
 790          $this->assertEquals(count($expectedusers), $search->get_total_participants_count());
 791  
 792          $rs = $search->get_participants();
 793          $this->assertInstanceOf(moodle_recordset::class, $rs);
 794  
 795          // Assert that each expected user is within the participant records.
 796          $records = $this->convert_recordset_to_array($rs);
 797          foreach ($expectedusers as $expecteduser) {
 798              $this->assertArrayHasKey($users[$expecteduser]->id, $records);
 799          }
 800      }
 801  
 802      /**
 803       * Data provider for {@see test_country_filter}
 804       *
 805       * @return array
 806       */
 807      public function country_provider(): array {
 808          $tests = [
 809              'users' => [
 810                  'user1' => 'DE',
 811                  'user2' => 'ES',
 812                  'user3' => 'ES',
 813                  'user4' => 'GB',
 814              ],
 815              'expects' => [
 816                  // Tests for jointype: ANY.
 817                  'ANY: No filter' => (object) [
 818                      'countries' => [],
 819                      'jointype' => filter::JOINTYPE_ANY,
 820                      'expectedusers' => [
 821                          'user1',
 822                          'user2',
 823                          'user3',
 824                          'user4',
 825                      ],
 826                  ],
 827                  'ANY: Matching filters' => (object) [
 828                      'countries' => [
 829                          'DE',
 830                          'GB',
 831                      ],
 832                      'jointype' => filter::JOINTYPE_ANY,
 833                      'expectedusers' => [
 834                          'user1',
 835                          'user4',
 836                      ],
 837                  ],
 838                  'ANY: Non-matching filters' => (object) [
 839                      'countries' => [
 840                          'RU',
 841                      ],
 842                      'jointype' => filter::JOINTYPE_ANY,
 843                      'expectedusers' => [],
 844                  ],
 845  
 846                  // Tests for jointype: ALL.
 847                  'ALL: No filter' => (object) [
 848                      'countries' => [],
 849                      'jointype' => filter::JOINTYPE_ALL,
 850                      'expectedusers' => [
 851                          'user1',
 852                          'user2',
 853                          'user3',
 854                          'user4',
 855                      ],
 856                  ],
 857                  'ALL: Matching filters' => (object) [
 858                      'countries' => [
 859                          'DE',
 860                          'GB',
 861                      ],
 862                      'jointype' => filter::JOINTYPE_ALL,
 863                      'expectedusers' => [
 864                          'user1',
 865                          'user4',
 866                      ],
 867                  ],
 868                  'ALL: Non-matching filters' => (object) [
 869                      'countries' => [
 870                          'RU',
 871                      ],
 872                      'jointype' => filter::JOINTYPE_ALL,
 873                      'expectedusers' => [],
 874                  ],
 875  
 876                  // Tests for jointype: NONE.
 877                  'NONE: No filter' => (object) [
 878                      'countries' => [],
 879                      'jointype' => filter::JOINTYPE_NONE,
 880                      'expectedusers' => [
 881                          'user1',
 882                          'user2',
 883                          'user3',
 884                          'user4',
 885                      ],
 886                  ],
 887                  'NONE: Matching filters' => (object) [
 888                      'countries' => [
 889                          'DE',
 890                          'GB',
 891                      ],
 892                      'jointype' => filter::JOINTYPE_NONE,
 893                      'expectedusers' => [
 894                          'user2',
 895                          'user3',
 896                      ],
 897                  ],
 898                  'NONE: Non-matching filters' => (object) [
 899                      'countries' => [
 900                          'RU',
 901                      ],
 902                      'jointype' => filter::JOINTYPE_NONE,
 903                      'expectedusers' => [
 904                          'user1',
 905                          'user2',
 906                          'user3',
 907                          'user4',
 908                      ],
 909                  ],
 910              ],
 911          ];
 912  
 913          $finaltests = [];
 914          foreach ($tests['expects'] as $testname => $test) {
 915              $finaltests[$testname] = [
 916                  'users' => $tests['users'],
 917                  'countries' => $test->countries,
 918                  'jointype' => $test->jointype,
 919                  'expectedusers' => $test->expectedusers,
 920              ];
 921          }
 922  
 923          return $finaltests;
 924      }
 925  
 926      /**
 927       * Ensure that the keywords filter works as expected with the provided test cases.
 928       *
 929       * @param array $usersdata The list of users to create
 930       * @param array $keywords The list of keywords to filter by
 931       * @param int $jointype The join type to use when combining filter values
 932       * @param int $count The expected count
 933       * @param array $expectedusers
 934       * @param string $asuser If non-blank, uses that user account (for identify field permission checks)
 935       * @dataProvider keywords_provider
 936       */
 937      public function test_keywords_filter(array $usersdata, array $keywords, int $jointype, int $count,
 938              array $expectedusers, string $asuser): void {
 939          global $DB;
 940  
 941          $course = $this->getDataGenerator()->create_course();
 942          $coursecontext = context_course::instance($course->id);
 943          $users = [];
 944  
 945          // Create the custom user profile field and put it into showuseridentity.
 946          $this->getDataGenerator()->create_custom_profile_field(
 947                  ['datatype' => 'text', 'shortname' => 'frog', 'name' => 'Fave frog']);
 948          set_config('showuseridentity', 'email,profile_field_frog');
 949  
 950          foreach ($usersdata as $username => $userdata) {
 951              // Prevent randomly generated field values that may cause false fails.
 952              $userdata['firstnamephonetic'] = $userdata['firstnamephonetic'] ?? $userdata['firstname'];
 953              $userdata['lastnamephonetic'] = $userdata['lastnamephonetic'] ?? $userdata['lastname'];
 954              $userdata['middlename'] = $userdata['middlename'] ?? '';
 955              $userdata['alternatename'] = $userdata['alternatename'] ?? $username;
 956  
 957              $user = $this->getDataGenerator()->create_user($userdata);
 958              $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
 959              $users[$username] = $user;
 960          }
 961  
 962          // Create a secondary course with users. We should not see these users.
 963          $this->create_course_with_users(10, 10, 10, 10);
 964  
 965          // Create the basic filter.
 966          $filterset = new participants_filterset();
 967          $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id]));
 968  
 969          // Create the keyword filter.
 970          $keywordfilter = new string_filter('keywords');
 971          $filterset->add_filter($keywordfilter);
 972  
 973          // Configure the filter.
 974          foreach ($keywords as $keyword) {
 975              $keywordfilter->add_filter_value($keyword);
 976          }
 977          $keywordfilter->set_join_type($jointype);
 978  
 979          if ($asuser) {
 980              $this->setUser($DB->get_record('user', ['username' => $asuser]));
 981          }
 982  
 983          // Run the search.
 984          $search = new participants_search($course, $coursecontext, $filterset);
 985          $rs = $search->get_participants();
 986          $this->assertInstanceOf(moodle_recordset::class, $rs);
 987          $records = $this->convert_recordset_to_array($rs);
 988  
 989          $this->assertCount($count, $records);
 990          $this->assertEquals($count, $search->get_total_participants_count());
 991  
 992          foreach ($expectedusers as $expecteduser) {
 993              $this->assertArrayHasKey($users[$expecteduser]->id, $records);
 994          }
 995      }
 996  
 997      /**
 998       * Data provider for keywords tests.
 999       *
1000       * @return array
1001       */
1002      public function keywords_provider(): array {
1003          $tests = [
1004              // Users where the keyword matches basic user fields such as names and email.
1005              'Users with basic names' => (object) [
1006                  'users' => [
1007                      'adam.ant' => [
1008                          'firstname' => 'Adam',
1009                          'lastname' => 'Ant',
1010                      ],
1011                      'barbara.bennett' => [
1012                          'firstname' => 'Barbara',
1013                          'lastname' => 'Bennett',
1014                          'alternatename' => 'Babs',
1015                          'firstnamephonetic' => 'Barbra',
1016                          'lastnamephonetic' => 'Benit',
1017                          'profile_field_frog' => 'Kermit',
1018                      ],
1019                      'colin.carnforth' => [
1020                          'firstname' => 'Colin',
1021                          'lastname' => 'Carnforth',
1022                          'middlename' => 'Jeffery',
1023                      ],
1024                      'tony.rogers' => [
1025                          'firstname' => 'Anthony',
1026                          'lastname' => 'Rogers',
1027                          'lastnamephonetic' => 'Rowjours',
1028                          'profile_field_frog' => 'Mr Toad',
1029                      ],
1030                      'sarah.rester' => [
1031                          'firstname' => 'Sarah',
1032                          'lastname' => 'Rester',
1033                          'email' => 'zazu@example.com',
1034                          'firstnamephonetic' => 'Sera',
1035                      ],
1036                  ],
1037                  'expect' => [
1038                      // Tests for jointype: ANY.
1039                      'ANY: No filter' => (object) [
1040                          'keywords' => [],
1041                          'jointype' => filter::JOINTYPE_ANY,
1042                          'count' => 5,
1043                          'expectedusers' => [
1044                              'adam.ant',
1045                              'barbara.bennett',
1046                              'colin.carnforth',
1047                              'tony.rogers',
1048                              'sarah.rester',
1049                          ],
1050                      ],
1051                      'ANY: Filter on first name only' => (object) [
1052                          'keywords' => ['adam'],
1053                          'jointype' => filter::JOINTYPE_ANY,
1054                          'count' => 1,
1055                          'expectedusers' => [
1056                              'adam.ant',
1057                          ],
1058                      ],
1059                      'ANY: Filter on last name only' => (object) [
1060                          'keywords' => ['BeNNeTt'],
1061                          'jointype' => filter::JOINTYPE_ANY,
1062                          'count' => 1,
1063                          'expectedusers' => [
1064                              'barbara.bennett',
1065                          ],
1066                      ],
1067                      'ANY: Filter on first/Last name' => (object) [
1068                          'keywords' => ['ant'],
1069                          'jointype' => filter::JOINTYPE_ANY,
1070                          'count' => 2,
1071                          'expectedusers' => [
1072                              'adam.ant',
1073                              'tony.rogers',
1074                          ],
1075                      ],
1076                      'ANY: Filter on fullname only' => (object) [
1077                          'keywords' => ['Barbara Bennett'],
1078                          'jointype' => filter::JOINTYPE_ANY,
1079                          'count' => 1,
1080                          'expectedusers' => [
1081                              'barbara.bennett',
1082                          ],
1083                      ],
1084                      'ANY: Filter on middlename only' => (object) [
1085                          'keywords' => ['Jeff'],
1086                          'jointype' => filter::JOINTYPE_ANY,
1087                          'count' => 1,
1088                          'expectedusers' => [
1089                              'colin.carnforth',
1090                          ],
1091                      ],
1092                      'ANY: Filter on username (no match)' => (object) [
1093                          'keywords' => ['sara.rester'],
1094                          'jointype' => filter::JOINTYPE_ANY,
1095                          'count' => 0,
1096                          'expectedusers' => [],
1097                      ],
1098                      'ANY: Filter on email only' => (object) [
1099                          'keywords' => ['zazu'],
1100                          'jointype' => filter::JOINTYPE_ANY,
1101                          'count' => 1,
1102                          'expectedusers' => [
1103                              'sarah.rester',
1104                          ],
1105                      ],
1106                      'ANY: Filter on first name phonetic only' => (object) [
1107                          'keywords' => ['Sera'],
1108                          'jointype' => filter::JOINTYPE_ANY,
1109                          'count' => 1,
1110                          'expectedusers' => [
1111                              'sarah.rester',
1112                          ],
1113                      ],
1114                      'ANY: Filter on last name phonetic only' => (object) [
1115                          'keywords' => ['jour'],
1116                          'jointype' => filter::JOINTYPE_ANY,
1117                          'count' => 1,
1118                          'expectedusers' => [
1119                              'tony.rogers',
1120                          ],
1121                      ],
1122                      'ANY: Filter on alternate name only' => (object) [
1123                          'keywords' => ['Babs'],
1124                          'jointype' => filter::JOINTYPE_ANY,
1125                          'count' => 1,
1126                          'expectedusers' => [
1127                              'barbara.bennett',
1128                          ],
1129                      ],
1130                      'ANY: Filter on multiple keywords (first/middle/last name)' => (object) [
1131                          'keywords' => ['ant', 'Jeff', 'rog'],
1132                          'jointype' => filter::JOINTYPE_ANY,
1133                          'count' => 3,
1134                          'expectedusers' => [
1135                              'adam.ant',
1136                              'colin.carnforth',
1137                              'tony.rogers',
1138                          ],
1139                      ],
1140                      'ANY: Filter on multiple keywords (phonetic/alternate names)' => (object) [
1141                          'keywords' => ['era', 'Bab', 'ours'],
1142                          'jointype' => filter::JOINTYPE_ANY,
1143                          'count' => 3,
1144                          'expectedusers' => [
1145                              'barbara.bennett',
1146                              'sarah.rester',
1147                              'tony.rogers',
1148                          ],
1149                      ],
1150                      'ANY: Filter on custom profile field' => (object) [
1151                          'keywords' => ['Kermit', 'Mr Toad'],
1152                          'jointype' => filter::JOINTYPE_ANY,
1153                          'count' => 2,
1154                          'expectedusers' => [
1155                              'barbara.bennett',
1156                              'tony.rogers',
1157                          ],
1158                          'asuser' => 'admin'
1159                      ],
1160                      'ANY: Filter on custom profile field (no permissions)' => (object) [
1161                          'keywords' => ['Kermit', 'Mr Toad'],
1162                          'jointype' => filter::JOINTYPE_ANY,
1163                          'count' => 0,
1164                          'expectedusers' => [],
1165                          'asuser' => 'barbara.bennett'
1166                      ],
1167  
1168                      // Tests for jointype: ALL.
1169                      'ALL: No filter' => (object) [
1170                          'keywords' => [],
1171                          'jointype' => filter::JOINTYPE_ALL,
1172                          'count' => 5,
1173                          'expectedusers' => [
1174                              'adam.ant',
1175                              'barbara.bennett',
1176                              'colin.carnforth',
1177                              'tony.rogers',
1178                              'sarah.rester',
1179                          ],
1180                      ],
1181                      'ALL: Filter on first name only' => (object) [
1182                          'keywords' => ['adam'],
1183                          'jointype' => filter::JOINTYPE_ALL,
1184                          'count' => 1,
1185                          'expectedusers' => [
1186                              'adam.ant',
1187                          ],
1188                      ],
1189                      'ALL: Filter on last name only' => (object) [
1190                          'keywords' => ['BeNNeTt'],
1191                          'jointype' => filter::JOINTYPE_ALL,
1192                          'count' => 1,
1193                          'expectedusers' => [
1194                              'barbara.bennett',
1195                          ],
1196                      ],
1197                      'ALL: Filter on first/Last name' => (object) [
1198                          'keywords' => ['ant'],
1199                          'jointype' => filter::JOINTYPE_ALL,
1200                          'count' => 2,
1201                          'expectedusers' => [
1202                              'adam.ant',
1203                              'tony.rogers',
1204                          ],
1205                      ],
1206                      'ALL: Filter on middlename only' => (object) [
1207                          'keywords' => ['Jeff'],
1208                          'jointype' => filter::JOINTYPE_ALL,
1209                          'count' => 1,
1210                          'expectedusers' => [
1211                              'colin.carnforth',
1212                          ],
1213                      ],
1214                      'ALL: Filter on username (no match)' => (object) [
1215                          'keywords' => ['sara.rester'],
1216                          'jointype' => filter::JOINTYPE_ALL,
1217                          'count' => 0,
1218                          'expectedusers' => [],
1219                      ],
1220                      'ALL: Filter on email only' => (object) [
1221                          'keywords' => ['zazu'],
1222                          'jointype' => filter::JOINTYPE_ALL,
1223                          'count' => 1,
1224                          'expectedusers' => [
1225                              'sarah.rester',
1226                          ],
1227                      ],
1228                      'ALL: Filter on first name phonetic only' => (object) [
1229                          'keywords' => ['Sera'],
1230                          'jointype' => filter::JOINTYPE_ALL,
1231                          'count' => 1,
1232                          'expectedusers' => [
1233                              'sarah.rester',
1234                          ],
1235                      ],
1236                      'ALL: Filter on last name phonetic only' => (object) [
1237                          'keywords' => ['jour'],
1238                          'jointype' => filter::JOINTYPE_ALL,
1239                          'count' => 1,
1240                          'expectedusers' => [
1241                              'tony.rogers',
1242                          ],
1243                      ],
1244                      'ALL: Filter on alternate name only' => (object) [
1245                          'keywords' => ['Babs'],
1246                          'jointype' => filter::JOINTYPE_ALL,
1247                          'count' => 1,
1248                          'expectedusers' => [
1249                              'barbara.bennett',
1250                          ],
1251                      ],
1252                      'ALL: Filter on multiple keywords (first/last name)' => (object) [
1253                          'keywords' => ['ant', 'rog'],
1254                          'jointype' => filter::JOINTYPE_ALL,
1255                          'count' => 1,
1256                          'expectedusers' => [
1257                              'tony.rogers',
1258                          ],
1259                      ],
1260                      'ALL: Filter on multiple keywords (first/middle/last name)' => (object) [
1261                          'keywords' => ['ant', 'Jeff', 'rog'],
1262                          'jointype' => filter::JOINTYPE_ALL,
1263                          'count' => 0,
1264                          'expectedusers' => [],
1265                      ],
1266                      'ALL: Filter on multiple keywords (phonetic/alternate names)' => (object) [
1267                          'keywords' => ['Bab', 'bra', 'nit'],
1268                          'jointype' => filter::JOINTYPE_ALL,
1269                          'count' => 1,
1270                          'expectedusers' => [
1271                              'barbara.bennett',
1272                          ],
1273                      ],
1274                      'ALL: Filter on custom profile field' => (object) [
1275                          'keywords' => ['Kermit', 'Kermi'],
1276                          'jointype' => filter::JOINTYPE_ALL,
1277                          'count' => 1,
1278                          'expectedusers' => [
1279                              'barbara.bennett',
1280                          ],
1281                          'asuser' => 'admin',
1282                      ],
1283                      'ALL: Filter on custom profile field (no permissions)' => (object) [
1284                          'keywords' => ['Kermit', 'Kermi'],
1285                          'jointype' => filter::JOINTYPE_ALL,
1286                          'count' => 0,
1287                          'expectedusers' => [],
1288                          'asuser' => 'barbara.bennett',
1289                      ],
1290  
1291                      // Tests for jointype: NONE.
1292                      'NONE: No filter' => (object) [
1293                          'keywords' => [],
1294                          'jointype' => filter::JOINTYPE_NONE,
1295                          'count' => 5,
1296                          'expectedusers' => [
1297                              'adam.ant',
1298                              'barbara.bennett',
1299                              'colin.carnforth',
1300                              'tony.rogers',
1301                              'sarah.rester',
1302                          ],
1303                      ],
1304                      'NONE: Filter on first name only' => (object) [
1305                          'keywords' => ['ara'],
1306                          'jointype' => filter::JOINTYPE_NONE,
1307                          'count' => 3,
1308                          'expectedusers' => [
1309                              'adam.ant',
1310                              'colin.carnforth',
1311                              'tony.rogers',
1312                          ],
1313                      ],
1314                      'NONE: Filter on last name only' => (object) [
1315                          'keywords' => ['BeNNeTt'],
1316                          'jointype' => filter::JOINTYPE_NONE,
1317                          'count' => 4,
1318                          'expectedusers' => [
1319                              'adam.ant',
1320                              'colin.carnforth',
1321                              'tony.rogers',
1322                              'sarah.rester',
1323                          ],
1324                      ],
1325                      'NONE: Filter on first/Last name' => (object) [
1326                          'keywords' => ['ar'],
1327                          'jointype' => filter::JOINTYPE_NONE,
1328                          'count' => 2,
1329                          'expectedusers' => [
1330                              'adam.ant',
1331                              'tony.rogers',
1332                          ],
1333                      ],
1334                      'NONE: Filter on middlename only' => (object) [
1335                          'keywords' => ['Jeff'],
1336                          'jointype' => filter::JOINTYPE_NONE,
1337                          'count' => 4,
1338                          'expectedusers' => [
1339                              'adam.ant',
1340                              'barbara.bennett',
1341                              'tony.rogers',
1342                              'sarah.rester',
1343                          ],
1344                      ],
1345                      'NONE: Filter on username (no match)' => (object) [
1346                          'keywords' => ['sara.rester'],
1347                          'jointype' => filter::JOINTYPE_NONE,
1348                          'count' => 5,
1349                          'expectedusers' => [
1350                              'adam.ant',
1351                              'barbara.bennett',
1352                              'colin.carnforth',
1353                              'tony.rogers',
1354                              'sarah.rester',
1355                          ],
1356                      ],
1357                      'NONE: Filter on email' => (object) [
1358                          'keywords' => ['zazu'],
1359                          'jointype' => filter::JOINTYPE_NONE,
1360                          'count' => 4,
1361                          'expectedusers' => [
1362                              'adam.ant',
1363                              'barbara.bennett',
1364                              'colin.carnforth',
1365                              'tony.rogers',
1366                          ],
1367                      ],
1368                      'NONE: Filter on first name phonetic only' => (object) [
1369                          'keywords' => ['Sera'],
1370                          'jointype' => filter::JOINTYPE_NONE,
1371                          'count' => 4,
1372                          'expectedusers' => [
1373                              'adam.ant',
1374                              'barbara.bennett',
1375                              'colin.carnforth',
1376                              'tony.rogers',
1377                          ],
1378                      ],
1379                      'NONE: Filter on last name phonetic only' => (object) [
1380                          'keywords' => ['jour'],
1381                          'jointype' => filter::JOINTYPE_NONE,
1382                          'count' => 4,
1383                          'expectedusers' => [
1384                              'adam.ant',
1385                              'barbara.bennett',
1386                              'colin.carnforth',
1387                              'sarah.rester',
1388                          ],
1389                      ],
1390                      'NONE: Filter on alternate name only' => (object) [
1391                          'keywords' => ['Babs'],
1392                          'jointype' => filter::JOINTYPE_NONE,
1393                          'count' => 4,
1394                          'expectedusers' => [
1395                              'adam.ant',
1396                              'colin.carnforth',
1397                              'tony.rogers',
1398                              'sarah.rester',
1399                          ],
1400                      ],
1401                      'NONE: Filter on multiple keywords (first/last name)' => (object) [
1402                          'keywords' => ['ara', 'rog'],
1403                          'jointype' => filter::JOINTYPE_NONE,
1404                          'count' => 2,
1405                          'expectedusers' => [
1406                              'adam.ant',
1407                              'colin.carnforth',
1408                          ],
1409                      ],
1410                      'NONE: Filter on multiple keywords (first/middle/last name)' => (object) [
1411                          'keywords' => ['ant', 'Jeff', 'rog'],
1412                          'jointype' => filter::JOINTYPE_NONE,
1413                          'count' => 2,
1414                          'expectedusers' => [
1415                              'barbara.bennett',
1416                              'sarah.rester',
1417                          ],
1418                      ],
1419                      'NONE: Filter on multiple keywords (phonetic/alternate names)' => (object) [
1420                          'keywords' => ['Bab', 'bra', 'nit'],
1421                          'jointype' => filter::JOINTYPE_NONE,
1422                          'count' => 4,
1423                          'expectedusers' => [
1424                              'adam.ant',
1425                              'colin.carnforth',
1426                              'tony.rogers',
1427                              'sarah.rester',
1428                          ],
1429                      ],
1430                      'NONE: Filter on custom profile field' => (object) [
1431                          'keywords' => ['Kermit', 'Mr Toad'],
1432                          'jointype' => filter::JOINTYPE_NONE,
1433                          'count' => 3,
1434                          'expectedusers' => [
1435                              'adam.ant',
1436                              'colin.carnforth',
1437                              'sarah.rester',
1438                          ],
1439                          'asuser' => 'admin',
1440                      ],
1441                      'NONE: Filter on custom profile field (no permissions)' => (object) [
1442                          'keywords' => ['Kermit', 'Mr Toad'],
1443                          'jointype' => filter::JOINTYPE_NONE,
1444                          'count' => 5,
1445                          'expectedusers' => [
1446                              'adam.ant',
1447                              'barbara.bennett',
1448                              'colin.carnforth',
1449                              'tony.rogers',
1450                              'sarah.rester',
1451                          ],
1452                          'asuser' => 'barbara.bennett',
1453                      ],
1454                  ],
1455              ],
1456          ];
1457  
1458          $finaltests = [];
1459          foreach ($tests as $testname => $testdata) {
1460              foreach ($testdata->expect as $expectname => $expectdata) {
1461                  $finaltests["{$testname} => {$expectname}"] = [
1462                      'users' => $testdata->users,
1463                      'keywords' => $expectdata->keywords,
1464                      'jointype' => $expectdata->jointype,
1465                      'count' => $expectdata->count,
1466                      'expectedusers' => $expectdata->expectedusers,
1467                      'asuser' => $expectdata->asuser ?? ''
1468                  ];
1469              }
1470          }
1471  
1472          return $finaltests;
1473      }
1474  
1475      /**
1476       * Ensure that the enrolment status filter works as expected with the provided test cases.
1477       *
1478       * @param array $usersdata The list of users to create
1479       * @param array $statuses The list of statuses to filter by
1480       * @param int $jointype The join type to use when combining filter values
1481       * @param int $count The expected count
1482       * @param array $expectedusers
1483       * @dataProvider status_provider
1484       */
1485      public function test_status_filter(array $usersdata, array $statuses, int $jointype, int $count, array $expectedusers): void {
1486          $course = $this->getDataGenerator()->create_course();
1487          $coursecontext = context_course::instance($course->id);
1488          $users = [];
1489  
1490          // Ensure sufficient capabilities to view all statuses.
1491          $this->setAdminUser();
1492  
1493          // Ensure all enrolment methods enabled.
1494          $enrolinstances = enrol_get_instances($course->id, false);
1495          foreach ($enrolinstances as $instance) {
1496              $plugin = enrol_get_plugin($instance->enrol);
1497              $plugin->update_status($instance, ENROL_INSTANCE_ENABLED);
1498          }
1499  
1500          foreach ($usersdata as $username => $userdata) {
1501              $user = $this->getDataGenerator()->create_user(['username' => $username]);
1502  
1503              if (array_key_exists('status', $userdata)) {
1504                  foreach ($userdata['status'] as $enrolmethod => $status) {
1505                      $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student', $enrolmethod, 0, 0, $status);
1506                  }
1507              }
1508  
1509              $users[$username] = $user;
1510          }
1511  
1512          // Create a secondary course with users. We should not see these users.
1513          $this->create_course_with_users(1, 1, 1, 1);
1514  
1515          // Create the basic filter.
1516          $filterset = new participants_filterset();
1517          $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id]));
1518  
1519          // Create the status filter.
1520          $statusfilter = new integer_filter('status');
1521          $filterset->add_filter($statusfilter);
1522  
1523          // Configure the filter.
1524          foreach ($statuses as $status) {
1525              $statusfilter->add_filter_value($status);
1526          }
1527          $statusfilter->set_join_type($jointype);
1528  
1529          // Run the search.
1530          $search = new participants_search($course, $coursecontext, $filterset);
1531          $rs = $search->get_participants();
1532          $this->assertInstanceOf(moodle_recordset::class, $rs);
1533          $records = $this->convert_recordset_to_array($rs);
1534  
1535          $this->assertCount($count, $records);
1536          $this->assertEquals($count, $search->get_total_participants_count());
1537  
1538          foreach ($expectedusers as $expecteduser) {
1539              $this->assertArrayHasKey($users[$expecteduser]->id, $records);
1540          }
1541      }
1542  
1543      /**
1544       * Data provider for status filter tests.
1545       *
1546       * @return array
1547       */
1548      public function status_provider(): array {
1549          $tests = [
1550              // Users with different statuses and enrolment methods (so multiple statuses are possible for the same user).
1551              'Users with different enrolment statuses' => (object) [
1552                  'users' => [
1553                      'a' => [
1554                          'status' => [
1555                              'manual' => ENROL_USER_ACTIVE,
1556                          ]
1557                      ],
1558                      'b' => [
1559                          'status' => [
1560                              'self' => ENROL_USER_ACTIVE,
1561                          ]
1562                      ],
1563                      'c' => [
1564                          'status' => [
1565                              'manual' => ENROL_USER_SUSPENDED,
1566                          ]
1567                      ],
1568                      'd' => [
1569                          'status' => [
1570                              'self' => ENROL_USER_SUSPENDED,
1571                          ]
1572                      ],
1573                      'e' => [
1574                          'status' => [
1575                              'manual' => ENROL_USER_ACTIVE,
1576                              'self' => ENROL_USER_SUSPENDED,
1577                          ]
1578                      ],
1579                  ],
1580                  'expect' => [
1581                      // Tests for jointype: ANY.
1582                      'ANY: No filter' => (object) [
1583                          'status' => [],
1584                          'jointype' => filter::JOINTYPE_ANY,
1585                          'count' => 5,
1586                          'expectedusers' => [
1587                              'a',
1588                              'b',
1589                              'c',
1590                              'd',
1591                              'e',
1592                          ],
1593                      ],
1594                      'ANY: Filter on active only' => (object) [
1595                          'status' => [ENROL_USER_ACTIVE],
1596                          'jointype' => filter::JOINTYPE_ANY,
1597                          'count' => 3,
1598                          'expectedusers' => [
1599                              'a',
1600                              'b',
1601                              'e',
1602                          ],
1603                      ],
1604                      'ANY: Filter on suspended only' => (object) [
1605                          'status' => [ENROL_USER_SUSPENDED],
1606                          'jointype' => filter::JOINTYPE_ANY,
1607                          'count' => 3,
1608                          'expectedusers' => [
1609                              'c',
1610                              'd',
1611                              'e',
1612                          ],
1613                      ],
1614                      'ANY: Filter on multiple statuses' => (object) [
1615                          'status' => [ENROL_USER_ACTIVE, ENROL_USER_SUSPENDED],
1616                          'jointype' => filter::JOINTYPE_ANY,
1617                          'count' => 5,
1618                          'expectedusers' => [
1619                              'a',
1620                              'b',
1621                              'c',
1622                              'd',
1623                              'e',
1624                          ],
1625                      ],
1626  
1627                      // Tests for jointype: ALL.
1628                      'ALL: No filter' => (object) [
1629                         'status' => [],
1630                          'jointype' => filter::JOINTYPE_ALL,
1631                          'count' => 5,
1632                          'expectedusers' => [
1633                              'a',
1634                              'b',
1635                              'c',
1636                              'd',
1637                              'e',
1638                          ],
1639                      ],
1640                      'ALL: Filter on active only' => (object) [
1641                          'status' => [ENROL_USER_ACTIVE],
1642                          'jointype' => filter::JOINTYPE_ALL,
1643                          'count' => 3,
1644                          'expectedusers' => [
1645                              'a',
1646                              'b',
1647                              'e',
1648                          ],
1649                      ],
1650                      'ALL: Filter on suspended only' => (object) [
1651                          'status' => [ENROL_USER_SUSPENDED],
1652                          'jointype' => filter::JOINTYPE_ALL,
1653                          'count' => 3,
1654                          'expectedusers' => [
1655                              'c',
1656                              'd',
1657                              'e',
1658                          ],
1659                      ],
1660                      'ALL: Filter on multiple statuses' => (object) [
1661                          'status' => [ENROL_USER_ACTIVE, ENROL_USER_SUSPENDED],
1662                          'jointype' => filter::JOINTYPE_ALL,
1663                          'count' => 1,
1664                          'expectedusers' => [
1665                              'e',
1666                          ],
1667                      ],
1668  
1669                      // Tests for jointype: NONE.
1670                      'NONE: No filter' => (object) [
1671                         'status' => [],
1672                          'jointype' => filter::JOINTYPE_NONE,
1673                          'count' => 5,
1674                          'expectedusers' => [
1675                              'a',
1676                              'b',
1677                              'c',
1678                              'd',
1679                              'e',
1680                          ],
1681                      ],
1682                      'NONE: Filter on active only' => (object) [
1683                          'status' => [ENROL_USER_ACTIVE],
1684                          'jointype' => filter::JOINTYPE_NONE,
1685                          'count' => 3,
1686                          'expectedusers' => [
1687                              'c',
1688                              'd',
1689                              'e',
1690                          ],
1691                      ],
1692                      'NONE: Filter on suspended only' => (object) [
1693                          'status' => [ENROL_USER_SUSPENDED],
1694                          'jointype' => filter::JOINTYPE_NONE,
1695                          'count' => 3,
1696                          'expectedusers' => [
1697                              'a',
1698                              'b',
1699                              'e',
1700                          ],
1701                      ],
1702                      'NONE: Filter on multiple statuses' => (object) [
1703                          'status' => [ENROL_USER_ACTIVE, ENROL_USER_SUSPENDED],
1704                          'jointype' => filter::JOINTYPE_NONE,
1705                          'count' => 0,
1706                          'expectedusers' => [],
1707                      ],
1708                  ],
1709              ],
1710          ];
1711  
1712          $finaltests = [];
1713          foreach ($tests as $testname => $testdata) {
1714              foreach ($testdata->expect as $expectname => $expectdata) {
1715                  $finaltests["{$testname} => {$expectname}"] = [
1716                      'users' => $testdata->users,
1717                      'status' => $expectdata->status,
1718                      'jointype' => $expectdata->jointype,
1719                      'count' => $expectdata->count,
1720                      'expectedusers' => $expectdata->expectedusers,
1721                  ];
1722              }
1723          }
1724  
1725          return $finaltests;
1726      }
1727  
1728      /**
1729       * Ensure that the enrolment methods filter works as expected with the provided test cases.
1730       *
1731       * @param array $usersdata The list of users to create
1732       * @param array $enrolmethods The list of enrolment methods to filter by
1733       * @param int $jointype The join type to use when combining filter values
1734       * @param int $count The expected count
1735       * @param array $expectedusers
1736       * @dataProvider enrolments_provider
1737       */
1738      public function test_enrolments_filter(array $usersdata, array $enrolmethods, int $jointype, int $count,
1739              array $expectedusers): void {
1740  
1741          $course = $this->getDataGenerator()->create_course();
1742          $coursecontext = context_course::instance($course->id);
1743          $users = [];
1744  
1745          // Ensure all enrolment methods enabled and mapped for setting the filter later.
1746          $enrolinstances = enrol_get_instances($course->id, false);
1747          $enrolinstancesmap = [];
1748          foreach ($enrolinstances as $instance) {
1749              $plugin = enrol_get_plugin($instance->enrol);
1750              $plugin->update_status($instance, ENROL_INSTANCE_ENABLED);
1751  
1752              $enrolinstancesmap[$instance->enrol] = (int) $instance->id;
1753          }
1754  
1755          foreach ($usersdata as $username => $userdata) {
1756              $user = $this->getDataGenerator()->create_user(['username' => $username]);
1757  
1758              if (array_key_exists('enrolmethods', $userdata)) {
1759                  foreach ($userdata['enrolmethods'] as $enrolmethod) {
1760                      $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student', $enrolmethod);
1761                  }
1762              }
1763  
1764              $users[$username] = $user;
1765          }
1766  
1767          // Create a secondary course with users. We should not see these users.
1768          $this->create_course_with_users(1, 1, 1, 1);
1769  
1770          // Create the basic filter.
1771          $filterset = new participants_filterset();
1772          $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id]));
1773  
1774          // Create the enrolment methods filter.
1775          $enrolmethodfilter = new integer_filter('enrolments');
1776          $filterset->add_filter($enrolmethodfilter);
1777  
1778          // Configure the filter.
1779          foreach ($enrolmethods as $enrolmethod) {
1780              $enrolmethodfilter->add_filter_value($enrolinstancesmap[$enrolmethod]);
1781          }
1782          $enrolmethodfilter->set_join_type($jointype);
1783  
1784          // Run the search.
1785          $search = new participants_search($course, $coursecontext, $filterset);
1786          $rs = $search->get_participants();
1787          $this->assertInstanceOf(moodle_recordset::class, $rs);
1788          $records = $this->convert_recordset_to_array($rs);
1789  
1790          $this->assertCount($count, $records);
1791          $this->assertEquals($count, $search->get_total_participants_count());
1792  
1793          foreach ($expectedusers as $expecteduser) {
1794              $this->assertArrayHasKey($users[$expecteduser]->id, $records);
1795          }
1796      }
1797  
1798      /**
1799       * Data provider for enrolments filter tests.
1800       *
1801       * @return array
1802       */
1803      public function enrolments_provider(): array {
1804          $tests = [
1805              // Users with different enrolment methods.
1806              'Users with different enrolment methods' => (object) [
1807                  'users' => [
1808                      'a' => [
1809                          'enrolmethods' => [
1810                              'manual',
1811                          ]
1812                      ],
1813                      'b' => [
1814                          'enrolmethods' => [
1815                              'self',
1816                          ]
1817                      ],
1818                      'c' => [
1819                          'enrolmethods' => [
1820                              'manual',
1821                              'self',
1822                          ]
1823                      ],
1824                  ],
1825                  'expect' => [
1826                      // Tests for jointype: ANY.
1827                      'ANY: No filter' => (object) [
1828                          'enrolmethods' => [],
1829                          'jointype' => filter::JOINTYPE_ANY,
1830                          'count' => 3,
1831                          'expectedusers' => [
1832                              'a',
1833                              'b',
1834                              'c',
1835                          ],
1836                      ],
1837                      'ANY: Filter by manual enrolments only' => (object) [
1838                          'enrolmethods' => ['manual'],
1839                          'jointype' => filter::JOINTYPE_ANY,
1840                          'count' => 2,
1841                          'expectedusers' => [
1842                              'a',
1843                              'c',
1844                          ],
1845                      ],
1846                      'ANY: Filter by self enrolments only' => (object) [
1847                          'enrolmethods' => ['self'],
1848                          'jointype' => filter::JOINTYPE_ANY,
1849                          'count' => 2,
1850                          'expectedusers' => [
1851                              'b',
1852                              'c',
1853                          ],
1854                      ],
1855                      'ANY: Filter by multiple enrolment methods' => (object) [
1856                          'enrolmethods' => ['manual', 'self'],
1857                          'jointype' => filter::JOINTYPE_ANY,
1858                          'count' => 3,
1859                          'expectedusers' => [
1860                              'a',
1861                              'b',
1862                              'c',
1863                          ],
1864                      ],
1865  
1866                      // Tests for jointype: ALL.
1867                      'ALL: No filter' => (object) [
1868                         'enrolmethods' => [],
1869                          'jointype' => filter::JOINTYPE_ALL,
1870                          'count' => 3,
1871                          'expectedusers' => [
1872                              'a',
1873                              'b',
1874                              'c',
1875                          ],
1876                      ],
1877                      'ALL: Filter by manual enrolments only' => (object) [
1878                          'enrolmethods' => ['manual'],
1879                          'jointype' => filter::JOINTYPE_ALL,
1880                          'count' => 2,
1881                          'expectedusers' => [
1882                              'a',
1883                              'c',
1884                          ],
1885                      ],
1886                      'ALL: Filter by multiple enrolment methods' => (object) [
1887                          'enrolmethods' => ['manual', 'self'],
1888                          'jointype' => filter::JOINTYPE_ALL,
1889                          'count' => 1,
1890                          'expectedusers' => [
1891                              'c',
1892                          ],
1893                      ],
1894  
1895                      // Tests for jointype: NONE.
1896                      'NONE: No filter' => (object) [
1897                         'enrolmethods' => [],
1898                          'jointype' => filter::JOINTYPE_NONE,
1899                          'count' => 3,
1900                          'expectedusers' => [
1901                              'a',
1902                              'b',
1903                              'c',
1904                          ],
1905                      ],
1906                      'NONE: Filter by manual enrolments only' => (object) [
1907                          'enrolmethods' => ['manual'],
1908                          'jointype' => filter::JOINTYPE_NONE,
1909                          'count' => 1,
1910                          'expectedusers' => [
1911                              'b',
1912                          ],
1913                      ],
1914                      'NONE: Filter by multiple enrolment methods' => (object) [
1915                          'enrolmethods' => ['manual', 'self'],
1916                          'jointype' => filter::JOINTYPE_NONE,
1917                          'count' => 0,
1918                          'expectedusers' => [],
1919                      ],
1920                  ],
1921              ],
1922          ];
1923  
1924          $finaltests = [];
1925          foreach ($tests as $testname => $testdata) {
1926              foreach ($testdata->expect as $expectname => $expectdata) {
1927                  $finaltests["{$testname} => {$expectname}"] = [
1928                      'users' => $testdata->users,
1929                      'enrolmethods' => $expectdata->enrolmethods,
1930                      'jointype' => $expectdata->jointype,
1931                      'count' => $expectdata->count,
1932                      'expectedusers' => $expectdata->expectedusers,
1933                  ];
1934              }
1935          }
1936  
1937          return $finaltests;
1938      }
1939  
1940      /**
1941       * Ensure that the groups filter works as expected with the provided test cases.
1942       *
1943       * @param array $usersdata The list of users to create
1944       * @param array $groupsavailable The names of groups that should be created in the course
1945       * @param array $filtergroups The names of groups to filter by
1946       * @param int $jointype The join type to use when combining filter values
1947       * @param int $count The expected count
1948       * @param array $expectedusers
1949       * @dataProvider groups_provider
1950       */
1951      public function test_groups_filter(array $usersdata, array $groupsavailable, array $filtergroups, int $jointype, int $count,
1952              array $expectedusers): void {
1953  
1954          $course = $this->getDataGenerator()->create_course();
1955          $coursecontext = context_course::instance($course->id);
1956          $users = [];
1957  
1958          // Prepare data for filtering by users in no groups.
1959          $nogroupsdata = (object) [
1960              'id' => USERSWITHOUTGROUP,
1961          ];
1962  
1963          // Map group names to group data.
1964           $groupsdata = ['nogroups' => $nogroupsdata];
1965          foreach ($groupsavailable as $groupname) {
1966              $groupinfo = [
1967                  'courseid' => $course->id,
1968                  'name' => $groupname,
1969              ];
1970  
1971              $groupsdata[$groupname] = $this->getDataGenerator()->create_group($groupinfo);
1972          }
1973  
1974          foreach ($usersdata as $username => $userdata) {
1975              $user = $this->getDataGenerator()->create_user(['username' => $username]);
1976              $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
1977  
1978              if (array_key_exists('groups', $userdata)) {
1979                  foreach ($userdata['groups'] as $groupname) {
1980                      $userinfo = [
1981                          'userid' => $user->id,
1982                          'groupid' => (int) $groupsdata[$groupname]->id,
1983                      ];
1984                      $this->getDataGenerator()->create_group_member($userinfo);
1985                  }
1986              }
1987  
1988              $users[$username] = $user;
1989          }
1990  
1991          // Create a secondary course with users. We should not see these users.
1992          $this->create_course_with_users(1, 1, 1, 1);
1993  
1994          // Create the basic filter.
1995          $filterset = new participants_filterset();
1996          $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id]));
1997  
1998          // Create the groups filter.
1999          $groupsfilter = new integer_filter('groups');
2000          $filterset->add_filter($groupsfilter);
2001  
2002          // Configure the filter.
2003          foreach ($filtergroups as $filtergroupname) {
2004              $groupsfilter->add_filter_value((int) $groupsdata[$filtergroupname]->id);
2005          }
2006          $groupsfilter->set_join_type($jointype);
2007  
2008          // Run the search.
2009          $search = new participants_search($course, $coursecontext, $filterset);
2010          $rs = $search->get_participants();
2011          $this->assertInstanceOf(moodle_recordset::class, $rs);
2012          $records = $this->convert_recordset_to_array($rs);
2013  
2014          $this->assertCount($count, $records);
2015          $this->assertEquals($count, $search->get_total_participants_count());
2016  
2017          foreach ($expectedusers as $expecteduser) {
2018              $this->assertArrayHasKey($users[$expecteduser]->id, $records);
2019          }
2020      }
2021  
2022      /**
2023       * Data provider for groups filter tests.
2024       *
2025       * @return array
2026       */
2027      public function groups_provider(): array {
2028          $tests = [
2029              'Users in different groups' => (object) [
2030                  'groupsavailable' => [
2031                      'groupa',
2032                      'groupb',
2033                      'groupc',
2034                  ],
2035                  'users' => [
2036                      'a' => [
2037                          'groups' => ['groupa'],
2038                      ],
2039                      'b' => [
2040                          'groups' => ['groupb'],
2041                      ],
2042                      'c' => [
2043                          'groups' => ['groupa', 'groupb'],
2044                      ],
2045                      'd' => [
2046                          'groups' => [],
2047                      ],
2048                  ],
2049                  'expect' => [
2050                      // Tests for jointype: ANY.
2051                      'ANY: No filter' => (object) [
2052                          'groups' => [],
2053                          'jointype' => filter::JOINTYPE_ANY,
2054                          'count' => 4,
2055                          'expectedusers' => [
2056                              'a',
2057                              'b',
2058                              'c',
2059                              'd',
2060                          ],
2061                      ],
2062                      'ANY: Filter on a single group' => (object) [
2063                          'groups' => ['groupa'],
2064                          'jointype' => filter::JOINTYPE_ANY,
2065                          'count' => 2,
2066                          'expectedusers' => [
2067                              'a',
2068                              'c',
2069                          ],
2070                      ],
2071                      'ANY: Filter on a group with no members' => (object) [
2072                          'groups' => ['groupc'],
2073                          'jointype' => filter::JOINTYPE_ANY,
2074                          'count' => 0,
2075                          'expectedusers' => [],
2076                      ],
2077                      'ANY: Filter on multiple groups' => (object) [
2078                          'groups' => ['groupa', 'groupb'],
2079                          'jointype' => filter::JOINTYPE_ANY,
2080                          'count' => 3,
2081                          'expectedusers' => [
2082                              'a',
2083                              'b',
2084                              'c',
2085                          ],
2086                      ],
2087                      'ANY: Filter on members of no groups only' => (object) [
2088                          'groups' => ['nogroups'],
2089                          'jointype' => filter::JOINTYPE_ANY,
2090                          'count' => 1,
2091                          'expectedusers' => [
2092                              'd',
2093                          ],
2094                      ],
2095                      'ANY: Filter on a single group or no groups' => (object) [
2096                          'groups' => ['groupa', 'nogroups'],
2097                          'jointype' => filter::JOINTYPE_ANY,
2098                          'count' => 3,
2099                          'expectedusers' => [
2100                              'a',
2101                              'c',
2102                              'd',
2103                          ],
2104                      ],
2105                      'ANY: Filter on multiple groups or no groups' => (object) [
2106                          'groups' => ['groupa', 'groupb', 'nogroups'],
2107                          'jointype' => filter::JOINTYPE_ANY,
2108                          'count' => 4,
2109                          'expectedusers' => [
2110                              'a',
2111                              'b',
2112                              'c',
2113                              'd',
2114                          ],
2115                      ],
2116  
2117                      // Tests for jointype: ALL.
2118                      'ALL: No filter' => (object) [
2119                          'groups' => [],
2120                          'jointype' => filter::JOINTYPE_ALL,
2121                          'count' => 4,
2122                          'expectedusers' => [
2123                              'a',
2124                              'b',
2125                              'c',
2126                              'd',
2127                          ],
2128                      ],
2129                      'ALL: Filter on a single group' => (object) [
2130                          'groups' => ['groupa'],
2131                          'jointype' => filter::JOINTYPE_ALL,
2132                          'count' => 2,
2133                          'expectedusers' => [
2134                              'a',
2135                              'c',
2136                          ],
2137                      ],
2138                      'ALL: Filter on a group with no members' => (object) [
2139                          'groups' => ['groupc'],
2140                          'jointype' => filter::JOINTYPE_ALL,
2141                          'count' => 0,
2142                          'expectedusers' => [],
2143                      ],
2144                      'ALL: Filter on members of no groups only' => (object) [
2145                          'groups' => ['nogroups'],
2146                          'jointype' => filter::JOINTYPE_ALL,
2147                          'count' => 1,
2148                          'expectedusers' => [
2149                              'd',
2150                          ],
2151                      ],
2152                      'ALL: Filter on multiple groups' => (object) [
2153                          'groups' => ['groupa', 'groupb'],
2154                          'jointype' => filter::JOINTYPE_ALL,
2155                          'count' => 1,
2156                          'expectedusers' => [
2157                              'c',
2158                          ],
2159                      ],
2160                      'ALL: Filter on a single group and no groups' => (object) [
2161                          'groups' => ['groupa', 'nogroups'],
2162                          'jointype' => filter::JOINTYPE_ALL,
2163                          'count' => 0,
2164                          'expectedusers' => [],
2165                      ],
2166                      'ALL: Filter on multiple groups and no groups' => (object) [
2167                          'groups' => ['groupa', 'groupb', 'nogroups'],
2168                          'jointype' => filter::JOINTYPE_ALL,
2169                          'count' => 0,
2170                          'expectedusers' => [],
2171                      ],
2172  
2173                      // Tests for jointype: NONE.
2174                      'NONE: No filter' => (object) [
2175                          'groups' => [],
2176                          'jointype' => filter::JOINTYPE_NONE,
2177                          'count' => 4,
2178                          'expectedusers' => [
2179                              'a',
2180                              'b',
2181                              'c',
2182                              'd',
2183                          ],
2184                      ],
2185                      'NONE: Filter on a single group' => (object) [
2186                          'groups' => ['groupa'],
2187                          'jointype' => filter::JOINTYPE_NONE,
2188                          'count' => 2,
2189                          'expectedusers' => [
2190                              'b',
2191                              'd',
2192                          ],
2193                      ],
2194                      'NONE: Filter on a group with no members' => (object) [
2195                          'groups' => ['groupc'],
2196                          'jointype' => filter::JOINTYPE_NONE,
2197                          'count' => 4,
2198                          'expectedusers' => [
2199                              'a',
2200                              'b',
2201                              'c',
2202                              'd',
2203                          ],
2204                      ],
2205                      'NONE: Filter on members of no groups only' => (object) [
2206                          'groups' => ['nogroups'],
2207                          'jointype' => filter::JOINTYPE_NONE,
2208                          'count' => 3,
2209                          'expectedusers' => [
2210                              'a',
2211                              'b',
2212                              'c',
2213                          ],
2214                      ],
2215                      'NONE: Filter on multiple groups' => (object) [
2216                          'groups' => ['groupa', 'groupb'],
2217                          'jointype' => filter::JOINTYPE_NONE,
2218                          'count' => 1,
2219                          'expectedusers' => [
2220                              'd',
2221                          ],
2222                      ],
2223                      'NONE: Filter on a single group and no groups' => (object) [
2224                          'groups' => ['groupa', 'nogroups'],
2225                          'jointype' => filter::JOINTYPE_NONE,
2226                          'count' => 1,
2227                          'expectedusers' => [
2228                              'b',
2229                          ],
2230                      ],
2231                      'NONE: Filter on multiple groups and no groups' => (object) [
2232                          'groups' => ['groupa', 'groupb', 'nogroups'],
2233                          'jointype' => filter::JOINTYPE_NONE,
2234                          'count' => 0,
2235                          'expectedusers' => [],
2236                      ],
2237                  ],
2238              ],
2239          ];
2240  
2241          $finaltests = [];
2242          foreach ($tests as $testname => $testdata) {
2243              foreach ($testdata->expect as $expectname => $expectdata) {
2244                  $finaltests["{$testname} => {$expectname}"] = [
2245                      'users' => $testdata->users,
2246                      'groupsavailable' => $testdata->groupsavailable,
2247                      'filtergroups' => $expectdata->groups,
2248                      'jointype' => $expectdata->jointype,
2249                      'count' => $expectdata->count,
2250                      'expectedusers' => $expectdata->expectedusers,
2251                  ];
2252              }
2253          }
2254  
2255          return $finaltests;
2256      }
2257  
2258      /**
2259       * Ensure that the groups filter works as expected when separate groups mode is enabled, with the provided test cases.
2260       *
2261       * @param array $usersdata The list of users to create
2262       * @param array $groupsavailable The names of groups that should be created in the course
2263       * @param array $filtergroups The names of groups to filter by
2264       * @param int $jointype The join type to use when combining filter values
2265       * @param int $count The expected count
2266       * @param array $expectedusers
2267       * @param string $loginusername The user to login as for the tests
2268       * @dataProvider groups_separate_provider
2269       */
2270      public function test_groups_filter_separate_groups(array $usersdata, array $groupsavailable, array $filtergroups, int $jointype,
2271              int $count, array $expectedusers, string $loginusername): void {
2272  
2273          $course = $this->getDataGenerator()->create_course();
2274          $coursecontext = context_course::instance($course->id);
2275          $users = [];
2276  
2277          // Enable separate groups mode on the course.
2278          $course->groupmode = SEPARATEGROUPS;
2279          $course->groupmodeforce = true;
2280          update_course($course);
2281  
2282          // Prepare data for filtering by users in no groups.
2283          $nogroupsdata = (object) [
2284              'id' => USERSWITHOUTGROUP,
2285          ];
2286  
2287          // Map group names to group data.
2288           $groupsdata = ['nogroups' => $nogroupsdata];
2289          foreach ($groupsavailable as $groupname) {
2290              $groupinfo = [
2291                  'courseid' => $course->id,
2292                  'name' => $groupname,
2293              ];
2294  
2295              $groupsdata[$groupname] = $this->getDataGenerator()->create_group($groupinfo);
2296          }
2297  
2298          foreach ($usersdata as $username => $userdata) {
2299              $user = $this->getDataGenerator()->create_user(['username' => $username]);
2300              $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
2301  
2302              if (array_key_exists('groups', $userdata)) {
2303                  foreach ($userdata['groups'] as $groupname) {
2304                      $userinfo = [
2305                          'userid' => $user->id,
2306                          'groupid' => (int) $groupsdata[$groupname]->id,
2307                      ];
2308                      $this->getDataGenerator()->create_group_member($userinfo);
2309                  }
2310              }
2311  
2312              $users[$username] = $user;
2313  
2314              if ($username == $loginusername) {
2315                  $loginuser = $user;
2316              }
2317          }
2318  
2319          // Create a secondary course with users. We should not see these users.
2320          $this->create_course_with_users(1, 1, 1, 1);
2321  
2322          // Log in as the user to be tested.
2323          $this->setUser($loginuser);
2324  
2325          // Create the basic filter.
2326          $filterset = new participants_filterset();
2327          $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id]));
2328  
2329          // Create the groups filter.
2330          $groupsfilter = new integer_filter('groups');
2331          $filterset->add_filter($groupsfilter);
2332  
2333          // Configure the filter.
2334          foreach ($filtergroups as $filtergroupname) {
2335              $groupsfilter->add_filter_value((int) $groupsdata[$filtergroupname]->id);
2336          }
2337          $groupsfilter->set_join_type($jointype);
2338  
2339          // Run the search.
2340          $search = new participants_search($course, $coursecontext, $filterset);
2341  
2342          // Tests on user in no groups should throw an exception as they are not supported (participants are not visible to them).
2343          if (in_array('exception', $expectedusers)) {
2344              $this->expectException(\coding_exception::class);
2345              $rs = $search->get_participants();
2346          } else {
2347              // All other cases are tested as normal.
2348              $rs = $search->get_participants();
2349              $this->assertInstanceOf(moodle_recordset::class, $rs);
2350              $records = $this->convert_recordset_to_array($rs);
2351  
2352              $this->assertCount($count, $records);
2353              $this->assertEquals($count, $search->get_total_participants_count());
2354  
2355              foreach ($expectedusers as $expecteduser) {
2356                  $this->assertArrayHasKey($users[$expecteduser]->id, $records);
2357              }
2358          }
2359      }
2360  
2361      /**
2362       * Data provider for groups filter tests.
2363       *
2364       * @return array
2365       */
2366      public function groups_separate_provider(): array {
2367          $tests = [
2368              'Users in different groups with separate groups mode enabled' => (object) [
2369                  'groupsavailable' => [
2370                      'groupa',
2371                      'groupb',
2372                      'groupc',
2373                  ],
2374                  'users' => [
2375                      'a' => [
2376                          'groups' => ['groupa'],
2377                      ],
2378                      'b' => [
2379                          'groups' => ['groupb'],
2380                      ],
2381                      'c' => [
2382                          'groups' => ['groupa', 'groupb'],
2383                      ],
2384                      'd' => [
2385                          'groups' => [],
2386                      ],
2387                  ],
2388                  'expect' => [
2389                      // Tests for jointype: ANY.
2390                      'ANY: No filter, user in one group' => (object) [
2391                          'loginuser' => 'a',
2392                          'groups' => [],
2393                          'jointype' => filter::JOINTYPE_ANY,
2394                          'count' => 2,
2395                          'expectedusers' => [
2396                              'a',
2397                              'c',
2398                          ],
2399                      ],
2400                      'ANY: No filter, user in multiple groups' => (object) [
2401                          'loginuser' => 'c',
2402                          'groups' => [],
2403                          'jointype' => filter::JOINTYPE_ANY,
2404                          'count' => 3,
2405                          'expectedusers' => [
2406                              'a',
2407                              'b',
2408                              'c',
2409                          ],
2410                      ],
2411                      'ANY: No filter, user in no groups' => (object) [
2412                          'loginuser' => 'd',
2413                          'groups' => [],
2414                          'jointype' => filter::JOINTYPE_ANY,
2415                          'count' => 0,
2416                          'expectedusers' => ['exception'],
2417                      ],
2418                      'ANY: Filter on a single group, user in one group' => (object) [
2419                          'loginuser' => 'a',
2420                          'groups' => ['groupa'],
2421                          'jointype' => filter::JOINTYPE_ANY,
2422                          'count' => 2,
2423                          'expectedusers' => [
2424                              'a',
2425                              'c',
2426                          ],
2427                      ],
2428                      'ANY: Filter on a single group, user in multple groups' => (object) [
2429                          'loginuser' => 'c',
2430                          'groups' => ['groupa'],
2431                          'jointype' => filter::JOINTYPE_ANY,
2432                          'count' => 2,
2433                          'expectedusers' => [
2434                              'a',
2435                              'c',
2436                          ],
2437                      ],
2438                      'ANY: Filter on a single group, user in no groups' => (object) [
2439                          'loginuser' => 'd',
2440                          'groups' => ['groupa'],
2441                          'jointype' => filter::JOINTYPE_ANY,
2442                          'count' => 0,
2443                          'expectedusers' => ['exception'],
2444                      ],
2445                      'ANY: Filter on multiple groups, user in one group (ignore invalid groups)' => (object) [
2446                          'loginuser' => 'a',
2447                          'groups' => ['groupa', 'groupb'],
2448                          'jointype' => filter::JOINTYPE_ANY,
2449                          'count' => 2,
2450                          'expectedusers' => [
2451                              'a',
2452                              'c',
2453                          ],
2454                      ],
2455                      'ANY: Filter on multiple groups, user in multiple groups' => (object) [
2456                          'loginuser' => 'c',
2457                          'groups' => ['groupa', 'groupb'],
2458                          'jointype' => filter::JOINTYPE_ANY,
2459                          'count' => 3,
2460                          'expectedusers' => [
2461                              'a',
2462                              'b',
2463                              'c',
2464                          ],
2465                      ],
2466                      'ANY: Filter on multiple groups or no groups, user in multiple groups (ignore no groups)' => (object) [
2467                          'loginuser' => 'c',
2468                          'groups' => ['groupa', 'groupb', 'nogroups'],
2469                          'jointype' => filter::JOINTYPE_ANY,
2470                          'count' => 3,
2471                          'expectedusers' => [
2472                              'a',
2473                              'b',
2474                              'c',
2475                          ],
2476                      ],
2477  
2478                      // Tests for jointype: ALL.
2479                      'ALL: No filter, user in one group' => (object) [
2480                          'loginuser' => 'a',
2481                          'groups' => [],
2482                          'jointype' => filter::JOINTYPE_ALL,
2483                          'count' => 2,
2484                          'expectedusers' => [
2485                              'a',
2486                              'c',
2487                          ],
2488                      ],
2489                      'ALL: No filter, user in multiple groups' => (object) [
2490                          'loginuser' => 'c',
2491                          'groups' => [],
2492                          'jointype' => filter::JOINTYPE_ALL,
2493                          'count' => 3,
2494                          'expectedusers' => [
2495                              'a',
2496                              'b',
2497                              'c',
2498                          ],
2499                      ],
2500                      'ALL: No filter, user in no groups' => (object) [
2501                          'loginuser' => 'd',
2502                          'groups' => [],
2503                          'jointype' => filter::JOINTYPE_ALL,
2504                          'count' => 0,
2505                          'expectedusers' => ['exception'],
2506                      ],
2507                      'ALL: Filter on a single group, user in one group' => (object) [
2508                          'loginuser' => 'a',
2509                          'groups' => ['groupa'],
2510                          'jointype' => filter::JOINTYPE_ALL,
2511                          'count' => 2,
2512                          'expectedusers' => [
2513                              'a',
2514                              'c',
2515                          ],
2516                      ],
2517                      'ALL: Filter on a single group, user in multple groups' => (object) [
2518                          'loginuser' => 'c',
2519                          'groups' => ['groupa'],
2520                          'jointype' => filter::JOINTYPE_ALL,
2521                          'count' => 2,
2522                          'expectedusers' => [
2523                              'a',
2524                              'c',
2525                          ],
2526                      ],
2527                      'ALL: Filter on a single group, user in no groups' => (object) [
2528                          'loginuser' => 'd',
2529                          'groups' => ['groupa'],
2530                          'jointype' => filter::JOINTYPE_ALL,
2531                          'count' => 0,
2532                          'expectedusers' => ['exception'],
2533                      ],
2534                      'ALL: Filter on multiple groups, user in one group (ignore invalid groups)' => (object) [
2535                          'loginuser' => 'a',
2536                          'groups' => ['groupa', 'groupb'],
2537                          'jointype' => filter::JOINTYPE_ALL,
2538                          'count' => 2,
2539                          'expectedusers' => [
2540                              'a',
2541                              'c',
2542                          ],
2543                      ],
2544                      'ALL: Filter on multiple groups, user in multiple groups' => (object) [
2545                          'loginuser' => 'c',
2546                          'groups' => ['groupa', 'groupb'],
2547                          'jointype' => filter::JOINTYPE_ALL,
2548                          'count' => 1,
2549                          'expectedusers' => [
2550                              'c',
2551                          ],
2552                      ],
2553                      'ALL: Filter on multiple groups or no groups, user in multiple groups (ignore no groups)' => (object) [
2554                          'loginuser' => 'c',
2555                          'groups' => ['groupa', 'groupb', 'nogroups'],
2556                          'jointype' => filter::JOINTYPE_ALL,
2557                          'count' => 1,
2558                          'expectedusers' => [
2559                              'c',
2560                          ],
2561                      ],
2562  
2563                      // Tests for jointype: NONE.
2564                      'NONE: No filter, user in one group' => (object) [
2565                          'loginuser' => 'a',
2566                          'groups' => [],
2567                          'jointype' => filter::JOINTYPE_NONE,
2568                          'count' => 2,
2569                          'expectedusers' => [
2570                              'a',
2571                              'c',
2572                          ],
2573                      ],
2574                      'NONE: No filter, user in multiple groups' => (object) [
2575                          'loginuser' => 'c',
2576                          'groups' => [],
2577                          'jointype' => filter::JOINTYPE_NONE,
2578                          'count' => 3,
2579                          'expectedusers' => [
2580                              'a',
2581                              'b',
2582                              'c',
2583                          ],
2584                      ],
2585                      'NONE: No filter, user in no groups' => (object) [
2586                          'loginuser' => 'd',
2587                          'groups' => [],
2588                          'jointype' => filter::JOINTYPE_NONE,
2589                          'count' => 0,
2590                          'expectedusers' => ['exception'],
2591                      ],
2592                      'NONE: Filter on a single group, user in one group' => (object) [
2593                          'loginuser' => 'a',
2594                          'groups' => ['groupa'],
2595                          'jointype' => filter::JOINTYPE_NONE,
2596                          'count' => 0,
2597                          'expectedusers' => [],
2598                      ],
2599                      'NONE: Filter on a single group, user in multple groups' => (object) [
2600                          'loginuser' => 'c',
2601                          'groups' => ['groupa'],
2602                          'jointype' => filter::JOINTYPE_NONE,
2603                          'count' => 1,
2604                          'expectedusers' => [
2605                              'b',
2606                          ],
2607                      ],
2608                      'NONE: Filter on a single group, user in no groups' => (object) [
2609                          'loginuser' => 'd',
2610                          'groups' => ['groupa'],
2611                          'jointype' => filter::JOINTYPE_NONE,
2612                          'count' => 0,
2613                          'expectedusers' => ['exception'],
2614                      ],
2615                      'NONE: Filter on multiple groups, user in one group (ignore invalid groups)' => (object) [
2616                          'loginuser' => 'a',
2617                          'groups' => ['groupa', 'groupb'],
2618                          'jointype' => filter::JOINTYPE_NONE,
2619                          'count' => 0,
2620                          'expectedusers' => [],
2621                      ],
2622                      'NONE: Filter on multiple groups, user in multiple groups' => (object) [
2623                          'loginuser' => 'c',
2624                          'groups' => ['groupa', 'groupb'],
2625                          'jointype' => filter::JOINTYPE_NONE,
2626                          'count' => 0,
2627                          'expectedusers' => [],
2628                      ],
2629                      'NONE: Filter on multiple groups or no groups, user in multiple groups (ignore no groups)' => (object) [
2630                          'loginuser' => 'c',
2631                          'groups' => ['groupa', 'groupb', 'nogroups'],
2632                          'jointype' => filter::JOINTYPE_NONE,
2633                          'count' => 0,
2634                          'expectedusers' => [],
2635                      ],
2636                  ],
2637              ],
2638          ];
2639  
2640          $finaltests = [];
2641          foreach ($tests as $testname => $testdata) {
2642              foreach ($testdata->expect as $expectname => $expectdata) {
2643                  $finaltests["{$testname} => {$expectname}"] = [
2644                      'users' => $testdata->users,
2645                      'groupsavailable' => $testdata->groupsavailable,
2646                      'filtergroups' => $expectdata->groups,
2647                      'jointype' => $expectdata->jointype,
2648                      'count' => $expectdata->count,
2649                      'expectedusers' => $expectdata->expectedusers,
2650                      'loginusername' => $expectdata->loginuser,
2651                  ];
2652              }
2653          }
2654  
2655          return $finaltests;
2656      }
2657  
2658  
2659      /**
2660       * Ensure that the last access filter works as expected with the provided test cases.
2661       *
2662       * @param array $usersdata The list of users to create
2663       * @param array $accesssince The last access data to filter by
2664       * @param int $jointype The join type to use when combining filter values
2665       * @param int $count The expected count
2666       * @param array $expectedusers
2667       * @dataProvider accesssince_provider
2668       */
2669      public function test_accesssince_filter(array $usersdata, array $accesssince, int $jointype, int $count,
2670              array $expectedusers): void {
2671  
2672          $course = $this->getDataGenerator()->create_course();
2673          $coursecontext = context_course::instance($course->id);
2674          $users = [];
2675  
2676          foreach ($usersdata as $username => $userdata) {
2677              $usertimestamp = empty($userdata['lastlogin']) ? 0 : strtotime($userdata['lastlogin']);
2678  
2679              $user = $this->getDataGenerator()->create_user(['username' => $username]);
2680              $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
2681  
2682              // Create the record of the user's last access to the course.
2683              if ($usertimestamp > 0) {
2684                  $this->getDataGenerator()->create_user_course_lastaccess($user, $course, $usertimestamp);
2685              }
2686  
2687              $users[$username] = $user;
2688          }
2689  
2690          // Create a secondary course with users. We should not see these users.
2691          $this->create_course_with_users(1, 1, 1, 1);
2692  
2693          // Create the basic filter.
2694          $filterset = new participants_filterset();
2695          $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id]));
2696  
2697          // Create the last access filter.
2698          $lastaccessfilter = new integer_filter('accesssince');
2699          $filterset->add_filter($lastaccessfilter);
2700  
2701          // Configure the filter.
2702          foreach ($accesssince as $accessstring) {
2703              $lastaccessfilter->add_filter_value(strtotime($accessstring));
2704          }
2705          $lastaccessfilter->set_join_type($jointype);
2706  
2707          // Run the search.
2708          $search = new participants_search($course, $coursecontext, $filterset);
2709          $rs = $search->get_participants();
2710          $this->assertInstanceOf(moodle_recordset::class, $rs);
2711          $records = $this->convert_recordset_to_array($rs);
2712  
2713          $this->assertCount($count, $records);
2714          $this->assertEquals($count, $search->get_total_participants_count());
2715  
2716          foreach ($expectedusers as $expecteduser) {
2717              $this->assertArrayHasKey($users[$expecteduser]->id, $records);
2718          }
2719      }
2720  
2721      /**
2722       * Data provider for last access filter tests.
2723       *
2724       * @return array
2725       */
2726      public function accesssince_provider(): array {
2727          $tests = [
2728              // Users with different last access times.
2729              'Users in different groups' => (object) [
2730                  'users' => [
2731                      'a' => [
2732                          'lastlogin' => '-3 days',
2733                      ],
2734                      'b' => [
2735                          'lastlogin' => '-2 weeks',
2736                      ],
2737                      'c' => [
2738                          'lastlogin' => '-5 months',
2739                      ],
2740                      'd' => [
2741                          'lastlogin' => '-11 months',
2742                      ],
2743                      'e' => [
2744                          // Never logged in.
2745                          'lastlogin' => '',
2746                      ],
2747                  ],
2748                  'expect' => [
2749                      // Tests for jointype: ANY.
2750                      'ANY: No filter' => (object) [
2751                          'accesssince' => [],
2752                          'jointype' => filter::JOINTYPE_ANY,
2753                          'count' => 5,
2754                          'expectedusers' => [
2755                              'a',
2756                              'b',
2757                              'c',
2758                              'd',
2759                              'e',
2760                          ],
2761                      ],
2762                      'ANY: Filter on last login more than 1 year ago' => (object) [
2763                          'accesssince' => ['-1 year'],
2764                          'jointype' => filter::JOINTYPE_ANY,
2765                          'count' => 1,
2766                          'expectedusers' => [
2767                              'e',
2768                          ],
2769                      ],
2770                      'ANY: Filter on last login more than 6 months ago' => (object) [
2771                          'accesssince' => ['-6 months'],
2772                          'jointype' => filter::JOINTYPE_ANY,
2773                          'count' => 2,
2774                          'expectedusers' => [
2775                              'd',
2776                              'e',
2777                          ],
2778                      ],
2779                      'ANY: Filter on last login more than 3 weeks ago' => (object) [
2780                          'accesssince' => ['-3 weeks'],
2781                          'jointype' => filter::JOINTYPE_ANY,
2782                          'count' => 3,
2783                          'expectedusers' => [
2784                              'c',
2785                              'd',
2786                              'e',
2787                          ],
2788                      ],
2789                      'ANY: Filter on last login more than 5 days ago' => (object) [
2790                          'accesssince' => ['-5 days'],
2791                          'jointype' => filter::JOINTYPE_ANY,
2792                          'count' => 4,
2793                          'expectedusers' => [
2794                              'b',
2795                              'c',
2796                              'd',
2797                              'e',
2798                          ],
2799                      ],
2800                      'ANY: Filter on last login more than 2 days ago' => (object) [
2801                          'accesssince' => ['-2 days'],
2802                          'jointype' => filter::JOINTYPE_ANY,
2803                          'count' => 5,
2804                          'expectedusers' => [
2805                              'a',
2806                              'b',
2807                              'c',
2808                              'd',
2809                              'e',
2810                          ],
2811                      ],
2812  
2813                      // Tests for jointype: ALL.
2814                      'ALL: No filter' => (object) [
2815                          'accesssince' => [],
2816                          'jointype' => filter::JOINTYPE_ALL,
2817                          'count' => 5,
2818                          'expectedusers' => [
2819                              'a',
2820                              'b',
2821                              'c',
2822                              'd',
2823                              'e',
2824                          ],
2825                      ],
2826                      'ALL: Filter on last login more than 1 year ago' => (object) [
2827                          'accesssince' => ['-1 year'],
2828                          'jointype' => filter::JOINTYPE_ALL,
2829                          'count' => 1,
2830                          'expectedusers' => [
2831                              'e',
2832                          ],
2833                      ],
2834                      'ALL: Filter on last login more than 6 months ago' => (object) [
2835                          'accesssince' => ['-6 months'],
2836                          'jointype' => filter::JOINTYPE_ALL,
2837                          'count' => 2,
2838                          'expectedusers' => [
2839                              'd',
2840                              'e',
2841                          ],
2842                      ],
2843                      'ALL: Filter on last login more than 3 weeks ago' => (object) [
2844                          'accesssince' => ['-3 weeks'],
2845                          'jointype' => filter::JOINTYPE_ALL,
2846                          'count' => 3,
2847                          'expectedusers' => [
2848                              'c',
2849                              'd',
2850                              'e',
2851                          ],
2852                      ],
2853                      'ALL: Filter on last login more than 5 days ago' => (object) [
2854                          'accesssince' => ['-5 days'],
2855                          'jointype' => filter::JOINTYPE_ALL,
2856                          'count' => 4,
2857                          'expectedusers' => [
2858                              'b',
2859                              'c',
2860                              'd',
2861                              'e',
2862                          ],
2863                      ],
2864                      'ALL: Filter on last login more than 2 days ago' => (object) [
2865                          'accesssince' => ['-2 days'],
2866                          'jointype' => filter::JOINTYPE_ALL,
2867                          'count' => 5,
2868                          'expectedusers' => [
2869                              'a',
2870                              'b',
2871                              'c',
2872                              'd',
2873                              'e',
2874                          ],
2875                      ],
2876  
2877                      // Tests for jointype: NONE.
2878                      'NONE: No filter' => (object) [
2879                          'accesssince' => [],
2880                          'jointype' => filter::JOINTYPE_NONE,
2881                          'count' => 5,
2882                          'expectedusers' => [
2883                              'a',
2884                              'b',
2885                              'c',
2886                              'd',
2887                              'e',
2888                          ],
2889                      ],
2890                      'NONE: Filter on last login more than 1 year ago' => (object) [
2891                          'accesssince' => ['-1 year'],
2892                          'jointype' => filter::JOINTYPE_NONE,
2893                          'count' => 4,
2894                          'expectedusers' => [
2895                              'a',
2896                              'b',
2897                              'c',
2898                              'd',
2899                          ],
2900                      ],
2901                      'NONE: Filter on last login more than 6 months ago' => (object) [
2902                          'accesssince' => ['-6 months'],
2903                          'jointype' => filter::JOINTYPE_NONE,
2904                          'count' => 3,
2905                          'expectedusers' => [
2906                              'a',
2907                              'b',
2908                              'c',
2909                          ],
2910                      ],
2911                      'NONE: Filter on last login more than 3 weeks ago' => (object) [
2912                          'accesssince' => ['-3 weeks'],
2913                          'jointype' => filter::JOINTYPE_NONE,
2914                          'count' => 2,
2915                          'expectedusers' => [
2916                              'a',
2917                              'b',
2918                          ],
2919                      ],
2920                      'NONE: Filter on last login more than 5 days ago' => (object) [
2921                          'accesssince' => ['-5 days'],
2922                          'jointype' => filter::JOINTYPE_NONE,
2923                          'count' => 1,
2924                          'expectedusers' => [
2925                              'a',
2926                          ],
2927                      ],
2928                      'NONE: Filter on last login more than 2 days ago' => (object) [
2929                          'accesssince' => ['-2 days'],
2930                          'jointype' => filter::JOINTYPE_NONE,
2931                          'count' => 0,
2932                          'expectedusers' => [],
2933                      ],
2934                  ],
2935              ],
2936          ];
2937  
2938          $finaltests = [];
2939          foreach ($tests as $testname => $testdata) {
2940              foreach ($testdata->expect as $expectname => $expectdata) {
2941                  $finaltests["{$testname} => {$expectname}"] = [
2942                      'users' => $testdata->users,
2943                      'accesssince' => $expectdata->accesssince,
2944                      'jointype' => $expectdata->jointype,
2945                      'count' => $expectdata->count,
2946                      'expectedusers' => $expectdata->expectedusers,
2947                  ];
2948              }
2949          }
2950  
2951          return $finaltests;
2952      }
2953  
2954      /**
2955       * Ensure that the joins between filters in the filterset work as expected with the provided test cases.
2956       *
2957       * @param array $usersdata The list of users to create
2958       * @param array $filterdata The data to filter by
2959       * @param array $groupsavailable The names of groups that should be created in the course
2960       * @param int $jointype The join type to used between each filter being applied
2961       * @param int $count The expected count
2962       * @param array $expectedusers
2963       * @dataProvider filterset_joins_provider
2964       */
2965      public function test_filterset_joins(array $usersdata, array $filterdata, array $groupsavailable, int $jointype, int $count,
2966              array $expectedusers): void {
2967          global $DB;
2968  
2969          // Ensure sufficient capabilities to view all statuses.
2970          $this->setAdminUser();
2971  
2972          // Remove the default role.
2973          set_config('roleid', 0, 'enrol_manual');
2974  
2975          $course = $this->getDataGenerator()->create_course();
2976          $coursecontext = context_course::instance($course->id);
2977          $roles = $DB->get_records_menu('role', [], '', 'shortname, id');
2978          $users = [];
2979  
2980          // Ensure all enrolment methods are enabled (and mapped where required for filtering later).
2981          $enrolinstances = enrol_get_instances($course->id, false);
2982          $enrolinstancesmap = [];
2983          foreach ($enrolinstances as $instance) {
2984              $plugin = enrol_get_plugin($instance->enrol);
2985              $plugin->update_status($instance, ENROL_INSTANCE_ENABLED);
2986  
2987              $enrolinstancesmap[$instance->enrol] = (int) $instance->id;
2988          }
2989  
2990          // Create the required course groups and mapping.
2991          $nogroupsdata = (object) [
2992              'id' => USERSWITHOUTGROUP,
2993          ];
2994  
2995           $groupsdata = ['nogroups' => $nogroupsdata];
2996          foreach ($groupsavailable as $groupname) {
2997              $groupinfo = [
2998                  'courseid' => $course->id,
2999                  'name' => $groupname,
3000              ];
3001  
3002              $groupsdata[$groupname] = $this->getDataGenerator()->create_group($groupinfo);
3003          }
3004  
3005          // Create test users.
3006          foreach ($usersdata as $username => $userdata) {
3007              $usertimestamp = empty($userdata['lastlogin']) ? 0 : strtotime($userdata['lastlogin']);
3008              unset($userdata['lastlogin']);
3009  
3010              // Prevent randomly generated field values that may cause false fails.
3011              $userdata['firstnamephonetic'] = $userdata['firstnamephonetic'] ?? $userdata['firstname'];
3012              $userdata['lastnamephonetic'] = $userdata['lastnamephonetic'] ?? $userdata['lastname'];
3013              $userdata['middlename'] = $userdata['middlename'] ?? '';
3014              $userdata['alternatename'] = $userdata['alternatename'] ?? $username;
3015  
3016              $user = $this->getDataGenerator()->create_user($userdata);
3017  
3018              foreach ($userdata['enrolments'] as $details) {
3019                  $this->getDataGenerator()->enrol_user($user->id, $course->id, $roles[$details['role']],
3020                          $details['method'], 0, 0, $details['status']);
3021              }
3022  
3023              foreach ($userdata['groups'] as $groupname) {
3024                  $userinfo = [
3025                      'userid' => $user->id,
3026                      'groupid' => (int) $groupsdata[$groupname]->id,
3027                  ];
3028                  $this->getDataGenerator()->create_group_member($userinfo);
3029              }
3030  
3031              if ($usertimestamp > 0) {
3032                  $this->getDataGenerator()->create_user_course_lastaccess($user, $course, $usertimestamp);
3033              }
3034  
3035              $users[$username] = $user;
3036          }
3037  
3038          // Create a secondary course with users. We should not see these users.
3039          $this->create_course_with_users(10, 10, 10, 10);
3040  
3041          // Create the basic filterset.
3042          $filterset = new participants_filterset();
3043          $filterset->set_join_type($jointype);
3044          $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id]));
3045  
3046          // Apply the keywords filter if required.
3047          if (array_key_exists('keywords', $filterdata)) {
3048              $keywordfilter = new string_filter('keywords');
3049              $filterset->add_filter($keywordfilter);
3050  
3051              foreach ($filterdata['keywords']['values'] as $keyword) {
3052                  $keywordfilter->add_filter_value($keyword);
3053              }
3054              $keywordfilter->set_join_type($filterdata['keywords']['jointype']);
3055          }
3056  
3057          // Apply enrolment methods filter if required.
3058          if (array_key_exists('enrolmethods', $filterdata)) {
3059              $enrolmethodfilter = new integer_filter('enrolments');
3060              $filterset->add_filter($enrolmethodfilter);
3061  
3062              foreach ($filterdata['enrolmethods']['values'] as $enrolmethod) {
3063                  $enrolmethodfilter->add_filter_value($enrolinstancesmap[$enrolmethod]);
3064              }
3065              $enrolmethodfilter->set_join_type($filterdata['enrolmethods']['jointype']);
3066          }
3067  
3068          // Apply roles filter if required.
3069          if (array_key_exists('courseroles', $filterdata)) {
3070              $rolefilter = new integer_filter('roles');
3071              $filterset->add_filter($rolefilter);
3072  
3073              foreach ($filterdata['courseroles']['values'] as $rolename) {
3074                  $rolefilter->add_filter_value((int) $roles[$rolename]);
3075              }
3076              $rolefilter->set_join_type($filterdata['courseroles']['jointype']);
3077          }
3078  
3079          // Apply status filter if required.
3080          if (array_key_exists('status', $filterdata)) {
3081              $statusfilter = new integer_filter('status');
3082              $filterset->add_filter($statusfilter);
3083  
3084              foreach ($filterdata['status']['values'] as $status) {
3085                  $statusfilter->add_filter_value($status);
3086              }
3087              $statusfilter->set_join_type($filterdata['status']['jointype']);
3088          }
3089  
3090          // Apply groups filter if required.
3091          if (array_key_exists('groups', $filterdata)) {
3092              $groupsfilter = new integer_filter('groups');
3093              $filterset->add_filter($groupsfilter);
3094  
3095              foreach ($filterdata['groups']['values'] as $filtergroupname) {
3096                  $groupsfilter->add_filter_value((int) $groupsdata[$filtergroupname]->id);
3097              }
3098              $groupsfilter->set_join_type($filterdata['groups']['jointype']);
3099          }
3100  
3101          // Apply last access filter if required.
3102          if (array_key_exists('accesssince', $filterdata)) {
3103              $lastaccessfilter = new integer_filter('accesssince');
3104              $filterset->add_filter($lastaccessfilter);
3105  
3106              foreach ($filterdata['accesssince']['values'] as $accessstring) {
3107                  $lastaccessfilter->add_filter_value(strtotime($accessstring));
3108              }
3109              $lastaccessfilter->set_join_type($filterdata['accesssince']['jointype']);
3110          }
3111  
3112          // Run the search.
3113          $search = new participants_search($course, $coursecontext, $filterset);
3114          $rs = $search->get_participants();
3115          $this->assertInstanceOf(moodle_recordset::class, $rs);
3116          $records = $this->convert_recordset_to_array($rs);
3117  
3118          $this->assertCount($count, $records);
3119          $this->assertEquals($count, $search->get_total_participants_count());
3120  
3121          foreach ($expectedusers as $expecteduser) {
3122              $this->assertArrayHasKey($users[$expecteduser]->id, $records);
3123          }
3124      }
3125  
3126      /**
3127       * Data provider for filterset join tests.
3128       *
3129       * @return array
3130       */
3131      public function filterset_joins_provider(): array {
3132          $tests = [
3133              // Users with different configurations.
3134              'Users with different configurations' => (object) [
3135                  'groupsavailable' => [
3136                      'groupa',
3137                      'groupb',
3138                      'groupc',
3139                  ],
3140                  'users' => [
3141                      'adam.ant' => [
3142                          'firstname' => 'Adam',
3143                          'lastname' => 'Ant',
3144                          'enrolments' => [
3145                              [
3146                                  'role' => 'student',
3147                                  'method' => 'manual',
3148                                  'status' => ENROL_USER_ACTIVE,
3149                              ],
3150                          ],
3151                          'groups' => ['groupa'],
3152                          'lastlogin' => '-3 days',
3153                      ],
3154                      'barbara.bennett' => [
3155                          'firstname' => 'Barbara',
3156                          'lastname' => 'Bennett',
3157                          'enrolments' => [
3158                              [
3159                                  'role' => 'student',
3160                                  'method' => 'manual',
3161                                  'status' => ENROL_USER_ACTIVE,
3162                              ],
3163                              [
3164                                  'role' => 'teacher',
3165                                  'method' => 'manual',
3166                                  'status' => ENROL_USER_ACTIVE,
3167                              ],
3168                          ],
3169                          'groups' => ['groupb'],
3170                          'lastlogin' => '-2 weeks',
3171                      ],
3172                      'colin.carnforth' => [
3173                          'firstname' => 'Colin',
3174                          'lastname' => 'Carnforth',
3175                          'enrolments' => [
3176                              [
3177                                  'role' => 'editingteacher',
3178                                  'method' => 'self',
3179                                  'status' => ENROL_USER_SUSPENDED,
3180                              ],
3181                          ],
3182                          'groups' => ['groupa', 'groupb'],
3183                          'lastlogin' => '-5 months',
3184                      ],
3185                      'tony.rogers' => [
3186                          'firstname' => 'Anthony',
3187                          'lastname' => 'Rogers',
3188                          'enrolments' => [
3189                              [
3190                                  'role' => 'editingteacher',
3191                                  'method' => 'self',
3192                                  'status' => ENROL_USER_SUSPENDED,
3193                              ],
3194                          ],
3195                          'groups' => [],
3196                          'lastlogin' => '-10 months',
3197                      ],
3198                      'sarah.rester' => [
3199                          'firstname' => 'Sarah',
3200                          'lastname' => 'Rester',
3201                          'email' => 'zazu@example.com',
3202                          'enrolments' => [
3203                              [
3204                                  'role' => 'teacher',
3205                                  'method' => 'manual',
3206                                  'status' => ENROL_USER_ACTIVE,
3207                              ],
3208                              [
3209                                  'role' => 'editingteacher',
3210                                  'method' => 'self',
3211                                  'status' => ENROL_USER_SUSPENDED,
3212                              ],
3213                          ],
3214                          'groups' => [],
3215                          'lastlogin' => '-11 months',
3216                      ],
3217                      'morgan.crikeyson' => [
3218                          'firstname' => 'Morgan',
3219                          'lastname' => 'Crikeyson',
3220                          'enrolments' => [
3221                              [
3222                                  'role' => 'teacher',
3223                                  'method' => 'manual',
3224                                  'status' => ENROL_USER_ACTIVE,
3225                              ],
3226                          ],
3227                          'groups' => ['groupa'],
3228                          'lastlogin' => '-1 week',
3229                      ],
3230                      'jonathan.bravo' => [
3231                          'firstname' => 'Jonathan',
3232                          'lastname' => 'Bravo',
3233                          'enrolments' => [
3234                              [
3235                                  'role' => 'student',
3236                                  'method' => 'manual',
3237                                  'status' => ENROL_USER_ACTIVE,
3238                              ],
3239                          ],
3240                          'groups' => [],
3241                          // Never logged in.
3242                          'lastlogin' => '',
3243                      ],
3244                  ],
3245                  'expect' => [
3246                      // Tests for jointype: ANY.
3247                      'ANY: No filters in filterset' => (object) [
3248                          'filterdata' => [],
3249                          'jointype' => filter::JOINTYPE_ANY,
3250                          'count' => 7,
3251                          'expectedusers' => [
3252                              'adam.ant',
3253                              'barbara.bennett',
3254                              'colin.carnforth',
3255                              'tony.rogers',
3256                              'sarah.rester',
3257                              'morgan.crikeyson',
3258                              'jonathan.bravo',
3259                          ],
3260                      ],
3261                      'ANY: Filterset containing a single filter type' => (object) [
3262                          'filterdata' => [
3263                              'enrolmethods' => [
3264                                  'values' => ['self'],
3265                                  'jointype' => filter::JOINTYPE_ANY,
3266                              ],
3267                          ],
3268                          'jointype' => filter::JOINTYPE_ANY,
3269                          'count' => 3,
3270                          'expectedusers' => [
3271                              'colin.carnforth',
3272                              'tony.rogers',
3273                              'sarah.rester',
3274                          ],
3275                      ],
3276                      'ANY: Filterset matching all filter types on different users' => (object) [
3277                          'filterdata' => [
3278                              // Match Adam only.
3279                              'keywords' => [
3280                                  'values' => ['adam'],
3281                                  'jointype' => filter::JOINTYPE_ALL,
3282                              ],
3283                              // Match Sarah only.
3284                              'enrolmethods' => [
3285                                  'values' => ['manual', 'self'],
3286                                  'jointype' => filter::JOINTYPE_ALL,
3287                              ],
3288                              // Match Barbara only.
3289                              'courseroles' => [
3290                                  'values' => ['student', 'teacher'],
3291                                  'jointype' => filter::JOINTYPE_ALL,
3292                              ],
3293                              // Match Sarah only.
3294                              'status' => [
3295                                  'values' => [ENROL_USER_ACTIVE, ENROL_USER_SUSPENDED],
3296                                  'jointype' => filter::JOINTYPE_ALL,
3297                              ],
3298                              // Match Colin only.
3299                              'groups' => [
3300                                  'values' => ['groupa', 'groupb'],
3301                                  'jointype' => filter::JOINTYPE_ALL,
3302                              ],
3303                              // Match Jonathan only.
3304                              'accesssince' => [
3305                                  'values' => ['-1 year'],
3306                                  'jointype' => filter::JOINTYPE_ALL,
3307                                  ],
3308                          ],
3309                          'jointype' => filter::JOINTYPE_ANY,
3310                          'count' => 5,
3311                          // Morgan and Tony are not matched, to confirm filtering is not just returning all users.
3312                          'expectedusers' => [
3313                              'adam.ant',
3314                              'barbara.bennett',
3315                              'colin.carnforth',
3316                              'sarah.rester',
3317                              'jonathan.bravo',
3318                          ],
3319                      ],
3320  
3321                      // Tests for jointype: ALL.
3322                      'ALL: No filters in filterset' => (object) [
3323                          'filterdata' => [],
3324                          'jointype' => filter::JOINTYPE_ALL,
3325                          'count' => 7,
3326                          'expectedusers' => [
3327                              'adam.ant',
3328                              'barbara.bennett',
3329                              'colin.carnforth',
3330                              'tony.rogers',
3331                              'sarah.rester',
3332                              'morgan.crikeyson',
3333                              'jonathan.bravo',
3334                          ],
3335                      ],
3336                      'ALL: Filterset containing a single filter type' => (object) [
3337                          'filterdata' => [
3338                              'enrolmethods' => [
3339                                  'values' => ['self'],
3340                                  'jointype' => filter::JOINTYPE_ANY,
3341                              ],
3342                          ],
3343                          'jointype' => filter::JOINTYPE_ALL,
3344                          'count' => 3,
3345                          'expectedusers' => [
3346                              'colin.carnforth',
3347                              'tony.rogers',
3348                              'sarah.rester',
3349                          ],
3350                      ],
3351                      'ALL: Filterset combining all filter types' => (object) [
3352                          'filterdata' => [
3353                              // Exclude Adam, Tony, Morgan and Jonathan.
3354                              'keywords' => [
3355                                  'values' => ['ar'],
3356                                  'jointype' => filter::JOINTYPE_ANY,
3357                              ],
3358                              // Exclude Colin and Tony.
3359                              'enrolmethods' => [
3360                                  'values' => ['manual'],
3361                                  'jointype' => filter::JOINTYPE_ANY,
3362                              ],
3363                              // Exclude Adam, Barbara and Jonathan.
3364                              'courseroles' => [
3365                                  'values' => ['student'],
3366                                  'jointype' => filter::JOINTYPE_NONE,
3367                              ],
3368                              // Exclude Colin and Tony.
3369                              'status' => [
3370                                  'values' => [ENROL_USER_ACTIVE],
3371                                  'jointype' => filter::JOINTYPE_ALL,
3372                              ],
3373                              // Exclude Barbara.
3374                              'groups' => [
3375                                  'values' => ['groupa', 'nogroups'],
3376                                  'jointype' => filter::JOINTYPE_ANY,
3377                              ],
3378                              // Exclude Adam, Colin and Barbara.
3379                              'accesssince' => [
3380                                  'values' => ['-6 months'],
3381                                  'jointype' => filter::JOINTYPE_ALL,
3382                                  ],
3383                          ],
3384                          'jointype' => filter::JOINTYPE_ALL,
3385                          'count' => 1,
3386                          'expectedusers' => [
3387                              'sarah.rester',
3388                          ],
3389                      ],
3390  
3391                      // Tests for jointype: NONE.
3392                      'NONE: No filters in filterset' => (object) [
3393                          'filterdata' => [],
3394                          'jointype' => filter::JOINTYPE_NONE,
3395                          'count' => 7,
3396                          'expectedusers' => [
3397                              'adam.ant',
3398                              'barbara.bennett',
3399                              'colin.carnforth',
3400                              'tony.rogers',
3401                              'sarah.rester',
3402                              'morgan.crikeyson',
3403                              'jonathan.bravo',
3404                          ],
3405                      ],
3406                      'NONE: Filterset containing a single filter type' => (object) [
3407                          'filterdata' => [
3408                              'enrolmethods' => [
3409                                  'values' => ['self'],
3410                                  'jointype' => filter::JOINTYPE_ANY,
3411                              ],
3412                          ],
3413                          'jointype' => filter::JOINTYPE_NONE,
3414                          'count' => 4,
3415                          'expectedusers' => [
3416                              'adam.ant',
3417                              'barbara.bennett',
3418                              'morgan.crikeyson',
3419                              'jonathan.bravo',
3420                          ],
3421                      ],
3422                      'NONE: Filterset combining all filter types' => (object) [
3423                          'filterdata' => [
3424                              // Excludes Adam.
3425                              'keywords' => [
3426                                  'values' => ['adam'],
3427                                  'jointype' => filter::JOINTYPE_ANY,
3428                              ],
3429                              // Excludes Colin, Tony and Sarah.
3430                              'enrolmethods' => [
3431                                  'values' => ['self'],
3432                                  'jointype' => filter::JOINTYPE_ANY,
3433                              ],
3434                              // Excludes Jonathan.
3435                              'courseroles' => [
3436                                  'values' => ['student'],
3437                                  'jointype' => filter::JOINTYPE_NONE,
3438                              ],
3439                              // Excludes Colin, Tony and Sarah.
3440                              'status' => [
3441                                  'values' => [ENROL_USER_SUSPENDED],
3442                                  'jointype' => filter::JOINTYPE_ALL,
3443                              ],
3444                              // Excludes Adam, Colin, Tony, Sarah, Morgan and Jonathan.
3445                              'groups' => [
3446                                  'values' => ['groupa', 'nogroups'],
3447                                  'jointype' => filter::JOINTYPE_ANY,
3448                              ],
3449                              // Excludes Tony and Sarah.
3450                              'accesssince' => [
3451                                  'values' => ['-6 months'],
3452                                  'jointype' => filter::JOINTYPE_ALL,
3453                              ],
3454                          ],
3455                          'jointype' => filter::JOINTYPE_NONE,
3456                          'count' => 1,
3457                          'expectedusers' => [
3458                              'barbara.bennett',
3459                          ],
3460                      ],
3461                      'NONE: Filterset combining several filter types and a double-negative on keyword' => (object) [
3462                          'jointype' => filter::JOINTYPE_NONE,
3463                          'filterdata' => [
3464                              // Note: This is a jointype NONE on the parent jointype NONE.
3465                              // The result therefore negated in this instance.
3466                              // Include Adam and Anthony.
3467                              'keywords' => [
3468                                  'values' => ['ant'],
3469                                  'jointype' => filter::JOINTYPE_NONE,
3470                              ],
3471                              // Excludes Tony.
3472                              'status' => [
3473                                  'values' => [ENROL_USER_SUSPENDED],
3474                                  'jointype' => filter::JOINTYPE_ALL,
3475                              ],
3476                          ],
3477                          'count' => 1,
3478                          'expectedusers' => [
3479                              'adam.ant',
3480                          ],
3481                      ],
3482                  ],
3483              ],
3484          ];
3485  
3486          $finaltests = [];
3487          foreach ($tests as $testname => $testdata) {
3488              foreach ($testdata->expect as $expectname => $expectdata) {
3489                  $finaltests["{$testname} => {$expectname}"] = [
3490                      'users' => $testdata->users,
3491                      'filterdata' => $expectdata->filterdata,
3492                      'groupsavailable' => $testdata->groupsavailable,
3493                      'jointype' => $expectdata->jointype,
3494                      'count' => $expectdata->count,
3495                      'expectedusers' => $expectdata->expectedusers,
3496                  ];
3497              }
3498          }
3499  
3500          return $finaltests;
3501      }
3502  }