Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403]

   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       * Ensure that the keywords filter works as expected with the provided test cases.
 762       *
 763       * @param array $usersdata The list of users to create
 764       * @param array $keywords The list of keywords to filter by
 765       * @param int $jointype The join type to use when combining filter values
 766       * @param int $count The expected count
 767       * @param array $expectedusers
 768       * @dataProvider keywords_provider
 769       */
 770      public function test_keywords_filter(array $usersdata, array $keywords, int $jointype, int $count, array $expectedusers): void {
 771          $course = $this->getDataGenerator()->create_course();
 772          $coursecontext = context_course::instance($course->id);
 773          $users = [];
 774  
 775          foreach ($usersdata as $username => $userdata) {
 776              // Prevent randomly generated field values that may cause false fails.
 777              $userdata['firstnamephonetic'] = $userdata['firstnamephonetic'] ?? $userdata['firstname'];
 778              $userdata['lastnamephonetic'] = $userdata['lastnamephonetic'] ?? $userdata['lastname'];
 779              $userdata['middlename'] = $userdata['middlename'] ?? '';
 780              $userdata['alternatename'] = $userdata['alternatename'] ?? $username;
 781  
 782              $user = $this->getDataGenerator()->create_user($userdata);
 783              $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
 784              $users[$username] = $user;
 785          }
 786  
 787          // Create a secondary course with users. We should not see these users.
 788          $this->create_course_with_users(10, 10, 10, 10);
 789  
 790          // Create the basic filter.
 791          $filterset = new participants_filterset();
 792          $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id]));
 793  
 794          // Create the keyword filter.
 795          $keywordfilter = new string_filter('keywords');
 796          $filterset->add_filter($keywordfilter);
 797  
 798          // Configure the filter.
 799          foreach ($keywords as $keyword) {
 800              $keywordfilter->add_filter_value($keyword);
 801          }
 802          $keywordfilter->set_join_type($jointype);
 803  
 804          // Run the search.
 805          $search = new participants_search($course, $coursecontext, $filterset);
 806          $rs = $search->get_participants();
 807          $this->assertInstanceOf(moodle_recordset::class, $rs);
 808          $records = $this->convert_recordset_to_array($rs);
 809  
 810          $this->assertCount($count, $records);
 811          $this->assertEquals($count, $search->get_total_participants_count());
 812  
 813          foreach ($expectedusers as $expecteduser) {
 814              $this->assertArrayHasKey($users[$expecteduser]->id, $records);
 815          }
 816      }
 817  
 818      /**
 819       * Data provider for keywords tests.
 820       *
 821       * @return array
 822       */
 823      public function keywords_provider(): array {
 824          $tests = [
 825              // Users where the keyword matches basic user fields such as names and email.
 826              'Users with basic names' => (object) [
 827                  'users' => [
 828                      'adam.ant' => [
 829                          'firstname' => 'Adam',
 830                          'lastname' => 'Ant',
 831                      ],
 832                      'barbara.bennett' => [
 833                          'firstname' => 'Barbara',
 834                          'lastname' => 'Bennett',
 835                          'alternatename' => 'Babs',
 836                          'firstnamephonetic' => 'Barbra',
 837                          'lastnamephonetic' => 'Benit',
 838                      ],
 839                      'colin.carnforth' => [
 840                          'firstname' => 'Colin',
 841                          'lastname' => 'Carnforth',
 842                          'middlename' => 'Jeffery',
 843                      ],
 844                      'tony.rogers' => [
 845                          'firstname' => 'Anthony',
 846                          'lastname' => 'Rogers',
 847                          'lastnamephonetic' => 'Rowjours',
 848                      ],
 849                      'sarah.rester' => [
 850                          'firstname' => 'Sarah',
 851                          'lastname' => 'Rester',
 852                          'email' => 'zazu@example.com',
 853                          'firstnamephonetic' => 'Sera',
 854                      ],
 855                  ],
 856                  'expect' => [
 857                      // Tests for jointype: ANY.
 858                      'ANY: No filter' => (object) [
 859                          'keywords' => [],
 860                          'jointype' => filter::JOINTYPE_ANY,
 861                          'count' => 5,
 862                          'expectedusers' => [
 863                              'adam.ant',
 864                              'barbara.bennett',
 865                              'colin.carnforth',
 866                              'tony.rogers',
 867                              'sarah.rester',
 868                          ],
 869                      ],
 870                      'ANY: Filter on first name only' => (object) [
 871                          'keywords' => ['adam'],
 872                          'jointype' => filter::JOINTYPE_ANY,
 873                          'count' => 1,
 874                          'expectedusers' => [
 875                              'adam.ant',
 876                          ],
 877                      ],
 878                      'ANY: Filter on last name only' => (object) [
 879                          'keywords' => ['BeNNeTt'],
 880                          'jointype' => filter::JOINTYPE_ANY,
 881                          'count' => 1,
 882                          'expectedusers' => [
 883                              'barbara.bennett',
 884                          ],
 885                      ],
 886                      'ANY: Filter on first/Last name' => (object) [
 887                          'keywords' => ['ant'],
 888                          'jointype' => filter::JOINTYPE_ANY,
 889                          'count' => 2,
 890                          'expectedusers' => [
 891                              'adam.ant',
 892                              'tony.rogers',
 893                          ],
 894                      ],
 895                      'ANY: Filter on middlename only' => (object) [
 896                          'keywords' => ['Jeff'],
 897                          'jointype' => filter::JOINTYPE_ANY,
 898                          'count' => 1,
 899                          'expectedusers' => [
 900                              'colin.carnforth',
 901                          ],
 902                      ],
 903                      'ANY: Filter on username (no match)' => (object) [
 904                          'keywords' => ['sara.rester'],
 905                          'jointype' => filter::JOINTYPE_ANY,
 906                          'count' => 0,
 907                          'expectedusers' => [],
 908                      ],
 909                      'ANY: Filter on email only' => (object) [
 910                          'keywords' => ['zazu'],
 911                          'jointype' => filter::JOINTYPE_ANY,
 912                          'count' => 1,
 913                          'expectedusers' => [
 914                              'sarah.rester',
 915                          ],
 916                      ],
 917                      'ANY: Filter on first name phonetic only' => (object) [
 918                          'keywords' => ['Sera'],
 919                          'jointype' => filter::JOINTYPE_ANY,
 920                          'count' => 1,
 921                          'expectedusers' => [
 922                              'sarah.rester',
 923                          ],
 924                      ],
 925                      'ANY: Filter on last name phonetic only' => (object) [
 926                          'keywords' => ['jour'],
 927                          'jointype' => filter::JOINTYPE_ANY,
 928                          'count' => 1,
 929                          'expectedusers' => [
 930                              'tony.rogers',
 931                          ],
 932                      ],
 933                      'ANY: Filter on alternate name only' => (object) [
 934                          'keywords' => ['Babs'],
 935                          'jointype' => filter::JOINTYPE_ANY,
 936                          'count' => 1,
 937                          'expectedusers' => [
 938                              'barbara.bennett',
 939                          ],
 940                      ],
 941                      'ANY: Filter on multiple keywords (first/middle/last name)' => (object) [
 942                          'keywords' => ['ant', 'Jeff', 'rog'],
 943                          'jointype' => filter::JOINTYPE_ANY,
 944                          'count' => 3,
 945                          'expectedusers' => [
 946                              'adam.ant',
 947                              'colin.carnforth',
 948                              'tony.rogers',
 949                          ],
 950                      ],
 951                      'ANY: Filter on multiple keywords (phonetic/alternate names)' => (object) [
 952                          'keywords' => ['era', 'Bab', 'ours'],
 953                          'jointype' => filter::JOINTYPE_ANY,
 954                          'count' => 3,
 955                          'expectedusers' => [
 956                              'barbara.bennett',
 957                              'sarah.rester',
 958                              'tony.rogers',
 959                          ],
 960                      ],
 961  
 962                      // Tests for jointype: ALL.
 963                      'ALL: No filter' => (object) [
 964                          'keywords' => [],
 965                          'jointype' => filter::JOINTYPE_ALL,
 966                          'count' => 5,
 967                          'expectedusers' => [
 968                              'adam.ant',
 969                              'barbara.bennett',
 970                              'colin.carnforth',
 971                              'tony.rogers',
 972                              'sarah.rester',
 973                          ],
 974                      ],
 975                      'ALL: Filter on first name only' => (object) [
 976                          'keywords' => ['adam'],
 977                          'jointype' => filter::JOINTYPE_ALL,
 978                          'count' => 1,
 979                          'expectedusers' => [
 980                              'adam.ant',
 981                          ],
 982                      ],
 983                      'ALL: Filter on last name only' => (object) [
 984                          'keywords' => ['BeNNeTt'],
 985                          'jointype' => filter::JOINTYPE_ALL,
 986                          'count' => 1,
 987                          'expectedusers' => [
 988                              'barbara.bennett',
 989                          ],
 990                      ],
 991                      'ALL: Filter on first/Last name' => (object) [
 992                          'keywords' => ['ant'],
 993                          'jointype' => filter::JOINTYPE_ALL,
 994                          'count' => 2,
 995                          'expectedusers' => [
 996                              'adam.ant',
 997                              'tony.rogers',
 998                          ],
 999                      ],
1000                      'ALL: Filter on middlename only' => (object) [
1001                          'keywords' => ['Jeff'],
1002                          'jointype' => filter::JOINTYPE_ALL,
1003                          'count' => 1,
1004                          'expectedusers' => [
1005                              'colin.carnforth',
1006                          ],
1007                      ],
1008                      'ALL: Filter on username (no match)' => (object) [
1009                          'keywords' => ['sara.rester'],
1010                          'jointype' => filter::JOINTYPE_ALL,
1011                          'count' => 0,
1012                          'expectedusers' => [],
1013                      ],
1014                      'ALL: Filter on email only' => (object) [
1015                          'keywords' => ['zazu'],
1016                          'jointype' => filter::JOINTYPE_ALL,
1017                          'count' => 1,
1018                          'expectedusers' => [
1019                              'sarah.rester',
1020                          ],
1021                      ],
1022                      'ALL: Filter on first name phonetic only' => (object) [
1023                          'keywords' => ['Sera'],
1024                          'jointype' => filter::JOINTYPE_ALL,
1025                          'count' => 1,
1026                          'expectedusers' => [
1027                              'sarah.rester',
1028                          ],
1029                      ],
1030                      'ALL: Filter on last name phonetic only' => (object) [
1031                          'keywords' => ['jour'],
1032                          'jointype' => filter::JOINTYPE_ALL,
1033                          'count' => 1,
1034                          'expectedusers' => [
1035                              'tony.rogers',
1036                          ],
1037                      ],
1038                      'ALL: Filter on alternate name only' => (object) [
1039                          'keywords' => ['Babs'],
1040                          'jointype' => filter::JOINTYPE_ALL,
1041                          'count' => 1,
1042                          'expectedusers' => [
1043                              'barbara.bennett',
1044                          ],
1045                      ],
1046                      'ALL: Filter on multiple keywords (first/last name)' => (object) [
1047                          'keywords' => ['ant', 'rog'],
1048                          'jointype' => filter::JOINTYPE_ALL,
1049                          'count' => 1,
1050                          'expectedusers' => [
1051                              'tony.rogers',
1052                          ],
1053                      ],
1054                      'ALL: Filter on multiple keywords (first/middle/last name)' => (object) [
1055                          'keywords' => ['ant', 'Jeff', 'rog'],
1056                          'jointype' => filter::JOINTYPE_ALL,
1057                          'count' => 0,
1058                          'expectedusers' => [],
1059                      ],
1060                      'ALL: Filter on multiple keywords (phonetic/alternate names)' => (object) [
1061                          'keywords' => ['Bab', 'bra', 'nit'],
1062                          'jointype' => filter::JOINTYPE_ALL,
1063                          'count' => 1,
1064                          'expectedusers' => [
1065                              'barbara.bennett',
1066                          ],
1067                      ],
1068  
1069                      // Tests for jointype: NONE.
1070                      'NONE: No filter' => (object) [
1071                          'keywords' => [],
1072                          'jointype' => filter::JOINTYPE_NONE,
1073                          'count' => 5,
1074                          'expectedusers' => [
1075                              'adam.ant',
1076                              'barbara.bennett',
1077                              'colin.carnforth',
1078                              'tony.rogers',
1079                              'sarah.rester',
1080                          ],
1081                      ],
1082                      'NONE: Filter on first name only' => (object) [
1083                          'keywords' => ['ara'],
1084                          'jointype' => filter::JOINTYPE_NONE,
1085                          'count' => 3,
1086                          'expectedusers' => [
1087                              'adam.ant',
1088                              'colin.carnforth',
1089                              'tony.rogers',
1090                          ],
1091                      ],
1092                      'NONE: Filter on last name only' => (object) [
1093                          'keywords' => ['BeNNeTt'],
1094                          'jointype' => filter::JOINTYPE_NONE,
1095                          'count' => 4,
1096                          'expectedusers' => [
1097                              'adam.ant',
1098                              'colin.carnforth',
1099                              'tony.rogers',
1100                              'sarah.rester',
1101                          ],
1102                      ],
1103                      'NONE: Filter on first/Last name' => (object) [
1104                          'keywords' => ['ar'],
1105                          'jointype' => filter::JOINTYPE_NONE,
1106                          'count' => 2,
1107                          'expectedusers' => [
1108                              'adam.ant',
1109                              'tony.rogers',
1110                          ],
1111                      ],
1112                      'NONE: Filter on middlename only' => (object) [
1113                          'keywords' => ['Jeff'],
1114                          'jointype' => filter::JOINTYPE_NONE,
1115                          'count' => 4,
1116                          'expectedusers' => [
1117                              'adam.ant',
1118                              'barbara.bennett',
1119                              'tony.rogers',
1120                              'sarah.rester',
1121                          ],
1122                      ],
1123                      'NONE: Filter on username (no match)' => (object) [
1124                          'keywords' => ['sara.rester'],
1125                          'jointype' => filter::JOINTYPE_NONE,
1126                          'count' => 5,
1127                          'expectedusers' => [
1128                              'adam.ant',
1129                              'barbara.bennett',
1130                              'colin.carnforth',
1131                              'tony.rogers',
1132                              'sarah.rester',
1133                          ],
1134                      ],
1135                      'NONE: Filter on email' => (object) [
1136                          'keywords' => ['zazu'],
1137                          'jointype' => filter::JOINTYPE_NONE,
1138                          'count' => 4,
1139                          'expectedusers' => [
1140                              'adam.ant',
1141                              'barbara.bennett',
1142                              'colin.carnforth',
1143                              'tony.rogers',
1144                          ],
1145                      ],
1146                      'NONE: Filter on first name phonetic only' => (object) [
1147                          'keywords' => ['Sera'],
1148                          'jointype' => filter::JOINTYPE_NONE,
1149                          'count' => 4,
1150                          'expectedusers' => [
1151                              'adam.ant',
1152                              'barbara.bennett',
1153                              'colin.carnforth',
1154                              'tony.rogers',
1155                          ],
1156                      ],
1157                      'NONE: Filter on last name phonetic only' => (object) [
1158                          'keywords' => ['jour'],
1159                          'jointype' => filter::JOINTYPE_NONE,
1160                          'count' => 4,
1161                          'expectedusers' => [
1162                              'adam.ant',
1163                              'barbara.bennett',
1164                              'colin.carnforth',
1165                              'sarah.rester',
1166                          ],
1167                      ],
1168                      'NONE: Filter on alternate name only' => (object) [
1169                          'keywords' => ['Babs'],
1170                          'jointype' => filter::JOINTYPE_NONE,
1171                          'count' => 4,
1172                          'expectedusers' => [
1173                              'adam.ant',
1174                              'colin.carnforth',
1175                              'tony.rogers',
1176                              'sarah.rester',
1177                          ],
1178                      ],
1179                      'NONE: Filter on multiple keywords (first/last name)' => (object) [
1180                          'keywords' => ['ara', 'rog'],
1181                          'jointype' => filter::JOINTYPE_NONE,
1182                          'count' => 2,
1183                          'expectedusers' => [
1184                              'adam.ant',
1185                              'colin.carnforth',
1186                          ],
1187                      ],
1188                      'NONE: Filter on multiple keywords (first/middle/last name)' => (object) [
1189                          'keywords' => ['ant', 'Jeff', 'rog'],
1190                          'jointype' => filter::JOINTYPE_NONE,
1191                          'count' => 2,
1192                          'expectedusers' => [
1193                              'barbara.bennett',
1194                              'sarah.rester',
1195                          ],
1196                      ],
1197                      'NONE: Filter on multiple keywords (phonetic/alternate names)' => (object) [
1198                          'keywords' => ['Bab', 'bra', 'nit'],
1199                          'jointype' => filter::JOINTYPE_NONE,
1200                          'count' => 4,
1201                          'expectedusers' => [
1202                              'adam.ant',
1203                              'colin.carnforth',
1204                              'tony.rogers',
1205                              'sarah.rester',
1206                          ],
1207                      ],
1208                  ],
1209              ],
1210          ];
1211  
1212          $finaltests = [];
1213          foreach ($tests as $testname => $testdata) {
1214              foreach ($testdata->expect as $expectname => $expectdata) {
1215                  $finaltests["{$testname} => {$expectname}"] = [
1216                      'users' => $testdata->users,
1217                      'keywords' => $expectdata->keywords,
1218                      'jointype' => $expectdata->jointype,
1219                      'count' => $expectdata->count,
1220                      'expectedusers' => $expectdata->expectedusers,
1221                  ];
1222              }
1223          }
1224  
1225          return $finaltests;
1226      }
1227  
1228      /**
1229       * Ensure that the enrolment status filter works as expected with the provided test cases.
1230       *
1231       * @param array $usersdata The list of users to create
1232       * @param array $statuses The list of statuses to filter by
1233       * @param int $jointype The join type to use when combining filter values
1234       * @param int $count The expected count
1235       * @param array $expectedusers
1236       * @dataProvider status_provider
1237       */
1238      public function test_status_filter(array $usersdata, array $statuses, int $jointype, int $count, array $expectedusers): void {
1239          $course = $this->getDataGenerator()->create_course();
1240          $coursecontext = context_course::instance($course->id);
1241          $users = [];
1242  
1243          // Ensure sufficient capabilities to view all statuses.
1244          $this->setAdminUser();
1245  
1246          // Ensure all enrolment methods enabled.
1247          $enrolinstances = enrol_get_instances($course->id, false);
1248          foreach ($enrolinstances as $instance) {
1249              $plugin = enrol_get_plugin($instance->enrol);
1250              $plugin->update_status($instance, ENROL_INSTANCE_ENABLED);
1251          }
1252  
1253          foreach ($usersdata as $username => $userdata) {
1254              $user = $this->getDataGenerator()->create_user(['username' => $username]);
1255  
1256              if (array_key_exists('status', $userdata)) {
1257                  foreach ($userdata['status'] as $enrolmethod => $status) {
1258                      $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student', $enrolmethod, 0, 0, $status);
1259                  }
1260              }
1261  
1262              $users[$username] = $user;
1263          }
1264  
1265          // Create a secondary course with users. We should not see these users.
1266          $this->create_course_with_users(1, 1, 1, 1);
1267  
1268          // Create the basic filter.
1269          $filterset = new participants_filterset();
1270          $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id]));
1271  
1272          // Create the status filter.
1273          $statusfilter = new integer_filter('status');
1274          $filterset->add_filter($statusfilter);
1275  
1276          // Configure the filter.
1277          foreach ($statuses as $status) {
1278              $statusfilter->add_filter_value($status);
1279          }
1280          $statusfilter->set_join_type($jointype);
1281  
1282          // Run the search.
1283          $search = new participants_search($course, $coursecontext, $filterset);
1284          $rs = $search->get_participants();
1285          $this->assertInstanceOf(moodle_recordset::class, $rs);
1286          $records = $this->convert_recordset_to_array($rs);
1287  
1288          $this->assertCount($count, $records);
1289          $this->assertEquals($count, $search->get_total_participants_count());
1290  
1291          foreach ($expectedusers as $expecteduser) {
1292              $this->assertArrayHasKey($users[$expecteduser]->id, $records);
1293          }
1294      }
1295  
1296      /**
1297       * Data provider for status filter tests.
1298       *
1299       * @return array
1300       */
1301      public function status_provider(): array {
1302          $tests = [
1303              // Users with different statuses and enrolment methods (so multiple statuses are possible for the same user).
1304              'Users with different enrolment statuses' => (object) [
1305                  'users' => [
1306                      'a' => [
1307                          'status' => [
1308                              'manual' => ENROL_USER_ACTIVE,
1309                          ]
1310                      ],
1311                      'b' => [
1312                          'status' => [
1313                              'self' => ENROL_USER_ACTIVE,
1314                          ]
1315                      ],
1316                      'c' => [
1317                          'status' => [
1318                              'manual' => ENROL_USER_SUSPENDED,
1319                          ]
1320                      ],
1321                      'd' => [
1322                          'status' => [
1323                              'self' => ENROL_USER_SUSPENDED,
1324                          ]
1325                      ],
1326                      'e' => [
1327                          'status' => [
1328                              'manual' => ENROL_USER_ACTIVE,
1329                              'self' => ENROL_USER_SUSPENDED,
1330                          ]
1331                      ],
1332                  ],
1333                  'expect' => [
1334                      // Tests for jointype: ANY.
1335                      'ANY: No filter' => (object) [
1336                          'status' => [],
1337                          'jointype' => filter::JOINTYPE_ANY,
1338                          'count' => 5,
1339                          'expectedusers' => [
1340                              'a',
1341                              'b',
1342                              'c',
1343                              'd',
1344                              'e',
1345                          ],
1346                      ],
1347                      'ANY: Filter on active only' => (object) [
1348                          'status' => [ENROL_USER_ACTIVE],
1349                          'jointype' => filter::JOINTYPE_ANY,
1350                          'count' => 3,
1351                          'expectedusers' => [
1352                              'a',
1353                              'b',
1354                              'e',
1355                          ],
1356                      ],
1357                      'ANY: Filter on suspended only' => (object) [
1358                          'status' => [ENROL_USER_SUSPENDED],
1359                          'jointype' => filter::JOINTYPE_ANY,
1360                          'count' => 3,
1361                          'expectedusers' => [
1362                              'c',
1363                              'd',
1364                              'e',
1365                          ],
1366                      ],
1367                      'ANY: Filter on multiple statuses' => (object) [
1368                          'status' => [ENROL_USER_ACTIVE, ENROL_USER_SUSPENDED],
1369                          'jointype' => filter::JOINTYPE_ANY,
1370                          'count' => 5,
1371                          'expectedusers' => [
1372                              'a',
1373                              'b',
1374                              'c',
1375                              'd',
1376                              'e',
1377                          ],
1378                      ],
1379  
1380                      // Tests for jointype: ALL.
1381                      'ALL: No filter' => (object) [
1382                         'status' => [],
1383                          'jointype' => filter::JOINTYPE_ALL,
1384                          'count' => 5,
1385                          'expectedusers' => [
1386                              'a',
1387                              'b',
1388                              'c',
1389                              'd',
1390                              'e',
1391                          ],
1392                      ],
1393                      'ALL: Filter on active only' => (object) [
1394                          'status' => [ENROL_USER_ACTIVE],
1395                          'jointype' => filter::JOINTYPE_ALL,
1396                          'count' => 3,
1397                          'expectedusers' => [
1398                              'a',
1399                              'b',
1400                              'e',
1401                          ],
1402                      ],
1403                      'ALL: Filter on suspended only' => (object) [
1404                          'status' => [ENROL_USER_SUSPENDED],
1405                          'jointype' => filter::JOINTYPE_ALL,
1406                          'count' => 3,
1407                          'expectedusers' => [
1408                              'c',
1409                              'd',
1410                              'e',
1411                          ],
1412                      ],
1413                      'ALL: Filter on multiple statuses' => (object) [
1414                          'status' => [ENROL_USER_ACTIVE, ENROL_USER_SUSPENDED],
1415                          'jointype' => filter::JOINTYPE_ALL,
1416                          'count' => 1,
1417                          'expectedusers' => [
1418                              'e',
1419                          ],
1420                      ],
1421  
1422                      // Tests for jointype: NONE.
1423                      'NONE: No filter' => (object) [
1424                         'status' => [],
1425                          'jointype' => filter::JOINTYPE_NONE,
1426                          'count' => 5,
1427                          'expectedusers' => [
1428                              'a',
1429                              'b',
1430                              'c',
1431                              'd',
1432                              'e',
1433                          ],
1434                      ],
1435                      'NONE: Filter on active only' => (object) [
1436                          'status' => [ENROL_USER_ACTIVE],
1437                          'jointype' => filter::JOINTYPE_NONE,
1438                          'count' => 3,
1439                          'expectedusers' => [
1440                              'c',
1441                              'd',
1442                              'e',
1443                          ],
1444                      ],
1445                      'NONE: Filter on suspended only' => (object) [
1446                          'status' => [ENROL_USER_SUSPENDED],
1447                          'jointype' => filter::JOINTYPE_NONE,
1448                          'count' => 3,
1449                          'expectedusers' => [
1450                              'a',
1451                              'b',
1452                              'e',
1453                          ],
1454                      ],
1455                      'NONE: Filter on multiple statuses' => (object) [
1456                          'status' => [ENROL_USER_ACTIVE, ENROL_USER_SUSPENDED],
1457                          'jointype' => filter::JOINTYPE_NONE,
1458                          'count' => 0,
1459                          'expectedusers' => [],
1460                      ],
1461                  ],
1462              ],
1463          ];
1464  
1465          $finaltests = [];
1466          foreach ($tests as $testname => $testdata) {
1467              foreach ($testdata->expect as $expectname => $expectdata) {
1468                  $finaltests["{$testname} => {$expectname}"] = [
1469                      'users' => $testdata->users,
1470                      'status' => $expectdata->status,
1471                      'jointype' => $expectdata->jointype,
1472                      'count' => $expectdata->count,
1473                      'expectedusers' => $expectdata->expectedusers,
1474                  ];
1475              }
1476          }
1477  
1478          return $finaltests;
1479      }
1480  
1481      /**
1482       * Ensure that the enrolment methods filter works as expected with the provided test cases.
1483       *
1484       * @param array $usersdata The list of users to create
1485       * @param array $enrolmethods The list of enrolment methods to filter by
1486       * @param int $jointype The join type to use when combining filter values
1487       * @param int $count The expected count
1488       * @param array $expectedusers
1489       * @dataProvider enrolments_provider
1490       */
1491      public function test_enrolments_filter(array $usersdata, array $enrolmethods, int $jointype, int $count,
1492              array $expectedusers): void {
1493  
1494          $course = $this->getDataGenerator()->create_course();
1495          $coursecontext = context_course::instance($course->id);
1496          $users = [];
1497  
1498          // Ensure all enrolment methods enabled and mapped for setting the filter later.
1499          $enrolinstances = enrol_get_instances($course->id, false);
1500          $enrolinstancesmap = [];
1501          foreach ($enrolinstances as $instance) {
1502              $plugin = enrol_get_plugin($instance->enrol);
1503              $plugin->update_status($instance, ENROL_INSTANCE_ENABLED);
1504  
1505              $enrolinstancesmap[$instance->enrol] = (int) $instance->id;
1506          }
1507  
1508          foreach ($usersdata as $username => $userdata) {
1509              $user = $this->getDataGenerator()->create_user(['username' => $username]);
1510  
1511              if (array_key_exists('enrolmethods', $userdata)) {
1512                  foreach ($userdata['enrolmethods'] as $enrolmethod) {
1513                      $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student', $enrolmethod);
1514                  }
1515              }
1516  
1517              $users[$username] = $user;
1518          }
1519  
1520          // Create a secondary course with users. We should not see these users.
1521          $this->create_course_with_users(1, 1, 1, 1);
1522  
1523          // Create the basic filter.
1524          $filterset = new participants_filterset();
1525          $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id]));
1526  
1527          // Create the enrolment methods filter.
1528          $enrolmethodfilter = new integer_filter('enrolments');
1529          $filterset->add_filter($enrolmethodfilter);
1530  
1531          // Configure the filter.
1532          foreach ($enrolmethods as $enrolmethod) {
1533              $enrolmethodfilter->add_filter_value($enrolinstancesmap[$enrolmethod]);
1534          }
1535          $enrolmethodfilter->set_join_type($jointype);
1536  
1537          // Run the search.
1538          $search = new participants_search($course, $coursecontext, $filterset);
1539          $rs = $search->get_participants();
1540          $this->assertInstanceOf(moodle_recordset::class, $rs);
1541          $records = $this->convert_recordset_to_array($rs);
1542  
1543          $this->assertCount($count, $records);
1544          $this->assertEquals($count, $search->get_total_participants_count());
1545  
1546          foreach ($expectedusers as $expecteduser) {
1547              $this->assertArrayHasKey($users[$expecteduser]->id, $records);
1548          }
1549      }
1550  
1551      /**
1552       * Data provider for enrolments filter tests.
1553       *
1554       * @return array
1555       */
1556      public function enrolments_provider(): array {
1557          $tests = [
1558              // Users with different enrolment methods.
1559              'Users with different enrolment methods' => (object) [
1560                  'users' => [
1561                      'a' => [
1562                          'enrolmethods' => [
1563                              'manual',
1564                          ]
1565                      ],
1566                      'b' => [
1567                          'enrolmethods' => [
1568                              'self',
1569                          ]
1570                      ],
1571                      'c' => [
1572                          'enrolmethods' => [
1573                              'manual',
1574                              'self',
1575                          ]
1576                      ],
1577                  ],
1578                  'expect' => [
1579                      // Tests for jointype: ANY.
1580                      'ANY: No filter' => (object) [
1581                          'enrolmethods' => [],
1582                          'jointype' => filter::JOINTYPE_ANY,
1583                          'count' => 3,
1584                          'expectedusers' => [
1585                              'a',
1586                              'b',
1587                              'c',
1588                          ],
1589                      ],
1590                      'ANY: Filter by manual enrolments only' => (object) [
1591                          'enrolmethods' => ['manual'],
1592                          'jointype' => filter::JOINTYPE_ANY,
1593                          'count' => 2,
1594                          'expectedusers' => [
1595                              'a',
1596                              'c',
1597                          ],
1598                      ],
1599                      'ANY: Filter by self enrolments only' => (object) [
1600                          'enrolmethods' => ['self'],
1601                          'jointype' => filter::JOINTYPE_ANY,
1602                          'count' => 2,
1603                          'expectedusers' => [
1604                              'b',
1605                              'c',
1606                          ],
1607                      ],
1608                      'ANY: Filter by multiple enrolment methods' => (object) [
1609                          'enrolmethods' => ['manual', 'self'],
1610                          'jointype' => filter::JOINTYPE_ANY,
1611                          'count' => 3,
1612                          'expectedusers' => [
1613                              'a',
1614                              'b',
1615                              'c',
1616                          ],
1617                      ],
1618  
1619                      // Tests for jointype: ALL.
1620                      'ALL: No filter' => (object) [
1621                         'enrolmethods' => [],
1622                          'jointype' => filter::JOINTYPE_ALL,
1623                          'count' => 3,
1624                          'expectedusers' => [
1625                              'a',
1626                              'b',
1627                              'c',
1628                          ],
1629                      ],
1630                      'ALL: Filter by manual enrolments only' => (object) [
1631                          'enrolmethods' => ['manual'],
1632                          'jointype' => filter::JOINTYPE_ALL,
1633                          'count' => 2,
1634                          'expectedusers' => [
1635                              'a',
1636                              'c',
1637                          ],
1638                      ],
1639                      'ALL: Filter by multiple enrolment methods' => (object) [
1640                          'enrolmethods' => ['manual', 'self'],
1641                          'jointype' => filter::JOINTYPE_ALL,
1642                          'count' => 1,
1643                          'expectedusers' => [
1644                              'c',
1645                          ],
1646                      ],
1647  
1648                      // Tests for jointype: NONE.
1649                      'NONE: No filter' => (object) [
1650                         'enrolmethods' => [],
1651                          'jointype' => filter::JOINTYPE_NONE,
1652                          'count' => 3,
1653                          'expectedusers' => [
1654                              'a',
1655                              'b',
1656                              'c',
1657                          ],
1658                      ],
1659                      'NONE: Filter by manual enrolments only' => (object) [
1660                          'enrolmethods' => ['manual'],
1661                          'jointype' => filter::JOINTYPE_NONE,
1662                          'count' => 1,
1663                          'expectedusers' => [
1664                              'b',
1665                          ],
1666                      ],
1667                      'NONE: Filter by multiple enrolment methods' => (object) [
1668                          'enrolmethods' => ['manual', 'self'],
1669                          'jointype' => filter::JOINTYPE_NONE,
1670                          'count' => 0,
1671                          'expectedusers' => [],
1672                      ],
1673                  ],
1674              ],
1675          ];
1676  
1677          $finaltests = [];
1678          foreach ($tests as $testname => $testdata) {
1679              foreach ($testdata->expect as $expectname => $expectdata) {
1680                  $finaltests["{$testname} => {$expectname}"] = [
1681                      'users' => $testdata->users,
1682                      'enrolmethods' => $expectdata->enrolmethods,
1683                      'jointype' => $expectdata->jointype,
1684                      'count' => $expectdata->count,
1685                      'expectedusers' => $expectdata->expectedusers,
1686                  ];
1687              }
1688          }
1689  
1690          return $finaltests;
1691      }
1692  
1693      /**
1694       * Ensure that the groups filter works as expected with the provided test cases.
1695       *
1696       * @param array $usersdata The list of users to create
1697       * @param array $groupsavailable The names of groups that should be created in the course
1698       * @param array $filtergroups The names of groups to filter by
1699       * @param int $jointype The join type to use when combining filter values
1700       * @param int $count The expected count
1701       * @param array $expectedusers
1702       * @dataProvider groups_provider
1703       */
1704      public function test_groups_filter(array $usersdata, array $groupsavailable, array $filtergroups, int $jointype, int $count,
1705              array $expectedusers): void {
1706  
1707          $course = $this->getDataGenerator()->create_course();
1708          $coursecontext = context_course::instance($course->id);
1709          $users = [];
1710  
1711          // Prepare data for filtering by users in no groups.
1712          $nogroupsdata = (object) [
1713              'id' => USERSWITHOUTGROUP,
1714          ];
1715  
1716          // Map group names to group data.
1717           $groupsdata = ['nogroups' => $nogroupsdata];
1718          foreach ($groupsavailable as $groupname) {
1719              $groupinfo = [
1720                  'courseid' => $course->id,
1721                  'name' => $groupname,
1722              ];
1723  
1724              $groupsdata[$groupname] = $this->getDataGenerator()->create_group($groupinfo);
1725          }
1726  
1727          foreach ($usersdata as $username => $userdata) {
1728              $user = $this->getDataGenerator()->create_user(['username' => $username]);
1729              $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
1730  
1731              if (array_key_exists('groups', $userdata)) {
1732                  foreach ($userdata['groups'] as $groupname) {
1733                      $userinfo = [
1734                          'userid' => $user->id,
1735                          'groupid' => (int) $groupsdata[$groupname]->id,
1736                      ];
1737                      $this->getDataGenerator()->create_group_member($userinfo);
1738                  }
1739              }
1740  
1741              $users[$username] = $user;
1742          }
1743  
1744          // Create a secondary course with users. We should not see these users.
1745          $this->create_course_with_users(1, 1, 1, 1);
1746  
1747          // Create the basic filter.
1748          $filterset = new participants_filterset();
1749          $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id]));
1750  
1751          // Create the groups filter.
1752          $groupsfilter = new integer_filter('groups');
1753          $filterset->add_filter($groupsfilter);
1754  
1755          // Configure the filter.
1756          foreach ($filtergroups as $filtergroupname) {
1757              $groupsfilter->add_filter_value((int) $groupsdata[$filtergroupname]->id);
1758          }
1759          $groupsfilter->set_join_type($jointype);
1760  
1761          // Run the search.
1762          $search = new participants_search($course, $coursecontext, $filterset);
1763          $rs = $search->get_participants();
1764          $this->assertInstanceOf(moodle_recordset::class, $rs);
1765          $records = $this->convert_recordset_to_array($rs);
1766  
1767          $this->assertCount($count, $records);
1768          $this->assertEquals($count, $search->get_total_participants_count());
1769  
1770          foreach ($expectedusers as $expecteduser) {
1771              $this->assertArrayHasKey($users[$expecteduser]->id, $records);
1772          }
1773      }
1774  
1775      /**
1776       * Data provider for groups filter tests.
1777       *
1778       * @return array
1779       */
1780      public function groups_provider(): array {
1781          $tests = [
1782              'Users in different groups' => (object) [
1783                  'groupsavailable' => [
1784                      'groupa',
1785                      'groupb',
1786                      'groupc',
1787                  ],
1788                  'users' => [
1789                      'a' => [
1790                          'groups' => ['groupa'],
1791                      ],
1792                      'b' => [
1793                          'groups' => ['groupb'],
1794                      ],
1795                      'c' => [
1796                          'groups' => ['groupa', 'groupb'],
1797                      ],
1798                      'd' => [
1799                          'groups' => [],
1800                      ],
1801                  ],
1802                  'expect' => [
1803                      // Tests for jointype: ANY.
1804                      'ANY: No filter' => (object) [
1805                          'groups' => [],
1806                          'jointype' => filter::JOINTYPE_ANY,
1807                          'count' => 4,
1808                          'expectedusers' => [
1809                              'a',
1810                              'b',
1811                              'c',
1812                              'd',
1813                          ],
1814                      ],
1815                      'ANY: Filter on a single group' => (object) [
1816                          'groups' => ['groupa'],
1817                          'jointype' => filter::JOINTYPE_ANY,
1818                          'count' => 2,
1819                          'expectedusers' => [
1820                              'a',
1821                              'c',
1822                          ],
1823                      ],
1824                      'ANY: Filter on a group with no members' => (object) [
1825                          'groups' => ['groupc'],
1826                          'jointype' => filter::JOINTYPE_ANY,
1827                          'count' => 0,
1828                          'expectedusers' => [],
1829                      ],
1830                      'ANY: Filter on multiple groups' => (object) [
1831                          'groups' => ['groupa', 'groupb'],
1832                          'jointype' => filter::JOINTYPE_ANY,
1833                          'count' => 3,
1834                          'expectedusers' => [
1835                              'a',
1836                              'b',
1837                              'c',
1838                          ],
1839                      ],
1840                      'ANY: Filter on members of no groups only' => (object) [
1841                          'groups' => ['nogroups'],
1842                          'jointype' => filter::JOINTYPE_ANY,
1843                          'count' => 1,
1844                          'expectedusers' => [
1845                              'd',
1846                          ],
1847                      ],
1848                      'ANY: Filter on a single group or no groups' => (object) [
1849                          'groups' => ['groupa', 'nogroups'],
1850                          'jointype' => filter::JOINTYPE_ANY,
1851                          'count' => 3,
1852                          'expectedusers' => [
1853                              'a',
1854                              'c',
1855                              'd',
1856                          ],
1857                      ],
1858                      'ANY: Filter on multiple groups or no groups' => (object) [
1859                          'groups' => ['groupa', 'groupb', 'nogroups'],
1860                          'jointype' => filter::JOINTYPE_ANY,
1861                          'count' => 4,
1862                          'expectedusers' => [
1863                              'a',
1864                              'b',
1865                              'c',
1866                              'd',
1867                          ],
1868                      ],
1869  
1870                      // Tests for jointype: ALL.
1871                      'ALL: No filter' => (object) [
1872                          'groups' => [],
1873                          'jointype' => filter::JOINTYPE_ALL,
1874                          'count' => 4,
1875                          'expectedusers' => [
1876                              'a',
1877                              'b',
1878                              'c',
1879                              'd',
1880                          ],
1881                      ],
1882                      'ALL: Filter on a single group' => (object) [
1883                          'groups' => ['groupa'],
1884                          'jointype' => filter::JOINTYPE_ALL,
1885                          'count' => 2,
1886                          'expectedusers' => [
1887                              'a',
1888                              'c',
1889                          ],
1890                      ],
1891                      'ALL: Filter on a group with no members' => (object) [
1892                          'groups' => ['groupc'],
1893                          'jointype' => filter::JOINTYPE_ALL,
1894                          'count' => 0,
1895                          'expectedusers' => [],
1896                      ],
1897                      'ALL: Filter on members of no groups only' => (object) [
1898                          'groups' => ['nogroups'],
1899                          'jointype' => filter::JOINTYPE_ALL,
1900                          'count' => 1,
1901                          'expectedusers' => [
1902                              'd',
1903                          ],
1904                      ],
1905                      'ALL: Filter on multiple groups' => (object) [
1906                          'groups' => ['groupa', 'groupb'],
1907                          'jointype' => filter::JOINTYPE_ALL,
1908                          'count' => 1,
1909                          'expectedusers' => [
1910                              'c',
1911                          ],
1912                      ],
1913                      'ALL: Filter on a single group and no groups' => (object) [
1914                          'groups' => ['groupa', 'nogroups'],
1915                          'jointype' => filter::JOINTYPE_ALL,
1916                          'count' => 0,
1917                          'expectedusers' => [],
1918                      ],
1919                      'ALL: Filter on multiple groups and no groups' => (object) [
1920                          'groups' => ['groupa', 'groupb', 'nogroups'],
1921                          'jointype' => filter::JOINTYPE_ALL,
1922                          'count' => 0,
1923                          'expectedusers' => [],
1924                      ],
1925  
1926                      // Tests for jointype: NONE.
1927                      'NONE: No filter' => (object) [
1928                          'groups' => [],
1929                          'jointype' => filter::JOINTYPE_NONE,
1930                          'count' => 4,
1931                          'expectedusers' => [
1932                              'a',
1933                              'b',
1934                              'c',
1935                              'd',
1936                          ],
1937                      ],
1938                      'NONE: Filter on a single group' => (object) [
1939                          'groups' => ['groupa'],
1940                          'jointype' => filter::JOINTYPE_NONE,
1941                          'count' => 2,
1942                          'expectedusers' => [
1943                              'b',
1944                              'd',
1945                          ],
1946                      ],
1947                      'NONE: Filter on a group with no members' => (object) [
1948                          'groups' => ['groupc'],
1949                          'jointype' => filter::JOINTYPE_NONE,
1950                          'count' => 4,
1951                          'expectedusers' => [
1952                              'a',
1953                              'b',
1954                              'c',
1955                              'd',
1956                          ],
1957                      ],
1958                      'NONE: Filter on members of no groups only' => (object) [
1959                          'groups' => ['nogroups'],
1960                          'jointype' => filter::JOINTYPE_NONE,
1961                          'count' => 3,
1962                          'expectedusers' => [
1963                              'a',
1964                              'b',
1965                              'c',
1966                          ],
1967                      ],
1968                      'NONE: Filter on multiple groups' => (object) [
1969                          'groups' => ['groupa', 'groupb'],
1970                          'jointype' => filter::JOINTYPE_NONE,
1971                          'count' => 1,
1972                          'expectedusers' => [
1973                              'd',
1974                          ],
1975                      ],
1976                      'NONE: Filter on a single group and no groups' => (object) [
1977                          'groups' => ['groupa', 'nogroups'],
1978                          'jointype' => filter::JOINTYPE_NONE,
1979                          'count' => 1,
1980                          'expectedusers' => [
1981                              'b',
1982                          ],
1983                      ],
1984                      'NONE: Filter on multiple groups and no groups' => (object) [
1985                          'groups' => ['groupa', 'groupb', 'nogroups'],
1986                          'jointype' => filter::JOINTYPE_NONE,
1987                          'count' => 0,
1988                          'expectedusers' => [],
1989                      ],
1990                  ],
1991              ],
1992          ];
1993  
1994          $finaltests = [];
1995          foreach ($tests as $testname => $testdata) {
1996              foreach ($testdata->expect as $expectname => $expectdata) {
1997                  $finaltests["{$testname} => {$expectname}"] = [
1998                      'users' => $testdata->users,
1999                      'groupsavailable' => $testdata->groupsavailable,
2000                      'filtergroups' => $expectdata->groups,
2001                      'jointype' => $expectdata->jointype,
2002                      'count' => $expectdata->count,
2003                      'expectedusers' => $expectdata->expectedusers,
2004                  ];
2005              }
2006          }
2007  
2008          return $finaltests;
2009      }
2010  
2011      /**
2012       * Ensure that the groups filter works as expected when separate groups mode is enabled, with the provided test cases.
2013       *
2014       * @param array $usersdata The list of users to create
2015       * @param array $groupsavailable The names of groups that should be created in the course
2016       * @param array $filtergroups The names of groups to filter by
2017       * @param int $jointype The join type to use when combining filter values
2018       * @param int $count The expected count
2019       * @param array $expectedusers
2020       * @param string $loginusername The user to login as for the tests
2021       * @dataProvider groups_separate_provider
2022       */
2023      public function test_groups_filter_separate_groups(array $usersdata, array $groupsavailable, array $filtergroups, int $jointype,
2024              int $count, array $expectedusers, string $loginusername): void {
2025  
2026          $course = $this->getDataGenerator()->create_course();
2027          $coursecontext = context_course::instance($course->id);
2028          $users = [];
2029  
2030          // Enable separate groups mode on the course.
2031          $course->groupmode = SEPARATEGROUPS;
2032          $course->groupmodeforce = true;
2033          update_course($course);
2034  
2035          // Prepare data for filtering by users in no groups.
2036          $nogroupsdata = (object) [
2037              'id' => USERSWITHOUTGROUP,
2038          ];
2039  
2040          // Map group names to group data.
2041           $groupsdata = ['nogroups' => $nogroupsdata];
2042          foreach ($groupsavailable as $groupname) {
2043              $groupinfo = [
2044                  'courseid' => $course->id,
2045                  'name' => $groupname,
2046              ];
2047  
2048              $groupsdata[$groupname] = $this->getDataGenerator()->create_group($groupinfo);
2049          }
2050  
2051          foreach ($usersdata as $username => $userdata) {
2052              $user = $this->getDataGenerator()->create_user(['username' => $username]);
2053              $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
2054  
2055              if (array_key_exists('groups', $userdata)) {
2056                  foreach ($userdata['groups'] as $groupname) {
2057                      $userinfo = [
2058                          'userid' => $user->id,
2059                          'groupid' => (int) $groupsdata[$groupname]->id,
2060                      ];
2061                      $this->getDataGenerator()->create_group_member($userinfo);
2062                  }
2063              }
2064  
2065              $users[$username] = $user;
2066  
2067              if ($username == $loginusername) {
2068                  $loginuser = $user;
2069              }
2070          }
2071  
2072          // Create a secondary course with users. We should not see these users.
2073          $this->create_course_with_users(1, 1, 1, 1);
2074  
2075          // Log in as the user to be tested.
2076          $this->setUser($loginuser);
2077  
2078          // Create the basic filter.
2079          $filterset = new participants_filterset();
2080          $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id]));
2081  
2082          // Create the groups filter.
2083          $groupsfilter = new integer_filter('groups');
2084          $filterset->add_filter($groupsfilter);
2085  
2086          // Configure the filter.
2087          foreach ($filtergroups as $filtergroupname) {
2088              $groupsfilter->add_filter_value((int) $groupsdata[$filtergroupname]->id);
2089          }
2090          $groupsfilter->set_join_type($jointype);
2091  
2092          // Run the search.
2093          $search = new participants_search($course, $coursecontext, $filterset);
2094  
2095          // Tests on user in no groups should throw an exception as they are not supported (participants are not visible to them).
2096          if (in_array('exception', $expectedusers)) {
2097              $this->expectException(\coding_exception::class);
2098              $rs = $search->get_participants();
2099          } else {
2100              // All other cases are tested as normal.
2101              $rs = $search->get_participants();
2102              $this->assertInstanceOf(moodle_recordset::class, $rs);
2103              $records = $this->convert_recordset_to_array($rs);
2104  
2105              $this->assertCount($count, $records);
2106              $this->assertEquals($count, $search->get_total_participants_count());
2107  
2108              foreach ($expectedusers as $expecteduser) {
2109                  $this->assertArrayHasKey($users[$expecteduser]->id, $records);
2110              }
2111          }
2112      }
2113  
2114      /**
2115       * Data provider for groups filter tests.
2116       *
2117       * @return array
2118       */
2119      public function groups_separate_provider(): array {
2120          $tests = [
2121              'Users in different groups with separate groups mode enabled' => (object) [
2122                  'groupsavailable' => [
2123                      'groupa',
2124                      'groupb',
2125                      'groupc',
2126                  ],
2127                  'users' => [
2128                      'a' => [
2129                          'groups' => ['groupa'],
2130                      ],
2131                      'b' => [
2132                          'groups' => ['groupb'],
2133                      ],
2134                      'c' => [
2135                          'groups' => ['groupa', 'groupb'],
2136                      ],
2137                      'd' => [
2138                          'groups' => [],
2139                      ],
2140                  ],
2141                  'expect' => [
2142                      // Tests for jointype: ANY.
2143                      'ANY: No filter, user in one group' => (object) [
2144                          'loginuser' => 'a',
2145                          'groups' => [],
2146                          'jointype' => filter::JOINTYPE_ANY,
2147                          'count' => 2,
2148                          'expectedusers' => [
2149                              'a',
2150                              'c',
2151                          ],
2152                      ],
2153                      'ANY: No filter, user in multiple groups' => (object) [
2154                          'loginuser' => 'c',
2155                          'groups' => [],
2156                          'jointype' => filter::JOINTYPE_ANY,
2157                          'count' => 3,
2158                          'expectedusers' => [
2159                              'a',
2160                              'b',
2161                              'c',
2162                          ],
2163                      ],
2164                      'ANY: No filter, user in no groups' => (object) [
2165                          'loginuser' => 'd',
2166                          'groups' => [],
2167                          'jointype' => filter::JOINTYPE_ANY,
2168                          'count' => 0,
2169                          'expectedusers' => ['exception'],
2170                      ],
2171                      'ANY: Filter on a single group, user in one group' => (object) [
2172                          'loginuser' => 'a',
2173                          'groups' => ['groupa'],
2174                          'jointype' => filter::JOINTYPE_ANY,
2175                          'count' => 2,
2176                          'expectedusers' => [
2177                              'a',
2178                              'c',
2179                          ],
2180                      ],
2181                      'ANY: Filter on a single group, user in multple groups' => (object) [
2182                          'loginuser' => 'c',
2183                          'groups' => ['groupa'],
2184                          'jointype' => filter::JOINTYPE_ANY,
2185                          'count' => 2,
2186                          'expectedusers' => [
2187                              'a',
2188                              'c',
2189                          ],
2190                      ],
2191                      'ANY: Filter on a single group, user in no groups' => (object) [
2192                          'loginuser' => 'd',
2193                          'groups' => ['groupa'],
2194                          'jointype' => filter::JOINTYPE_ANY,
2195                          'count' => 0,
2196                          'expectedusers' => ['exception'],
2197                      ],
2198                      'ANY: Filter on multiple groups, user in one group (ignore invalid groups)' => (object) [
2199                          'loginuser' => 'a',
2200                          'groups' => ['groupa', 'groupb'],
2201                          'jointype' => filter::JOINTYPE_ANY,
2202                          'count' => 2,
2203                          'expectedusers' => [
2204                              'a',
2205                              'c',
2206                          ],
2207                      ],
2208                      'ANY: Filter on multiple groups, user in multiple groups' => (object) [
2209                          'loginuser' => 'c',
2210                          'groups' => ['groupa', 'groupb'],
2211                          'jointype' => filter::JOINTYPE_ANY,
2212                          'count' => 3,
2213                          'expectedusers' => [
2214                              'a',
2215                              'b',
2216                              'c',
2217                          ],
2218                      ],
2219                      'ANY: Filter on multiple groups or no groups, user in multiple groups (ignore no groups)' => (object) [
2220                          'loginuser' => 'c',
2221                          'groups' => ['groupa', 'groupb', 'nogroups'],
2222                          'jointype' => filter::JOINTYPE_ANY,
2223                          'count' => 3,
2224                          'expectedusers' => [
2225                              'a',
2226                              'b',
2227                              'c',
2228                          ],
2229                      ],
2230  
2231                      // Tests for jointype: ALL.
2232                      'ALL: No filter, user in one group' => (object) [
2233                          'loginuser' => 'a',
2234                          'groups' => [],
2235                          'jointype' => filter::JOINTYPE_ALL,
2236                          'count' => 2,
2237                          'expectedusers' => [
2238                              'a',
2239                              'c',
2240                          ],
2241                      ],
2242                      'ALL: No filter, user in multiple groups' => (object) [
2243                          'loginuser' => 'c',
2244                          'groups' => [],
2245                          'jointype' => filter::JOINTYPE_ALL,
2246                          'count' => 3,
2247                          'expectedusers' => [
2248                              'a',
2249                              'b',
2250                              'c',
2251                          ],
2252                      ],
2253                      'ALL: No filter, user in no groups' => (object) [
2254                          'loginuser' => 'd',
2255                          'groups' => [],
2256                          'jointype' => filter::JOINTYPE_ALL,
2257                          'count' => 0,
2258                          'expectedusers' => ['exception'],
2259                      ],
2260                      'ALL: Filter on a single group, user in one group' => (object) [
2261                          'loginuser' => 'a',
2262                          'groups' => ['groupa'],
2263                          'jointype' => filter::JOINTYPE_ALL,
2264                          'count' => 2,
2265                          'expectedusers' => [
2266                              'a',
2267                              'c',
2268                          ],
2269                      ],
2270                      'ALL: Filter on a single group, user in multple groups' => (object) [
2271                          'loginuser' => 'c',
2272                          'groups' => ['groupa'],
2273                          'jointype' => filter::JOINTYPE_ALL,
2274                          'count' => 2,
2275                          'expectedusers' => [
2276                              'a',
2277                              'c',
2278                          ],
2279                      ],
2280                      'ALL: Filter on a single group, user in no groups' => (object) [
2281                          'loginuser' => 'd',
2282                          'groups' => ['groupa'],
2283                          'jointype' => filter::JOINTYPE_ALL,
2284                          'count' => 0,
2285                          'expectedusers' => ['exception'],
2286                      ],
2287                      'ALL: Filter on multiple groups, user in one group (ignore invalid groups)' => (object) [
2288                          'loginuser' => 'a',
2289                          'groups' => ['groupa', 'groupb'],
2290                          'jointype' => filter::JOINTYPE_ALL,
2291                          'count' => 2,
2292                          'expectedusers' => [
2293                              'a',
2294                              'c',
2295                          ],
2296                      ],
2297                      'ALL: Filter on multiple groups, user in multiple groups' => (object) [
2298                          'loginuser' => 'c',
2299                          'groups' => ['groupa', 'groupb'],
2300                          'jointype' => filter::JOINTYPE_ALL,
2301                          'count' => 1,
2302                          'expectedusers' => [
2303                              'c',
2304                          ],
2305                      ],
2306                      'ALL: Filter on multiple groups or no groups, user in multiple groups (ignore no groups)' => (object) [
2307                          'loginuser' => 'c',
2308                          'groups' => ['groupa', 'groupb', 'nogroups'],
2309                          'jointype' => filter::JOINTYPE_ALL,
2310                          'count' => 1,
2311                          'expectedusers' => [
2312                              'c',
2313                          ],
2314                      ],
2315  
2316                      // Tests for jointype: NONE.
2317                      'NONE: No filter, user in one group' => (object) [
2318                          'loginuser' => 'a',
2319                          'groups' => [],
2320                          'jointype' => filter::JOINTYPE_NONE,
2321                          'count' => 2,
2322                          'expectedusers' => [
2323                              'a',
2324                              'c',
2325                          ],
2326                      ],
2327                      'NONE: No filter, user in multiple groups' => (object) [
2328                          'loginuser' => 'c',
2329                          'groups' => [],
2330                          'jointype' => filter::JOINTYPE_NONE,
2331                          'count' => 3,
2332                          'expectedusers' => [
2333                              'a',
2334                              'b',
2335                              'c',
2336                          ],
2337                      ],
2338                      'NONE: No filter, user in no groups' => (object) [
2339                          'loginuser' => 'd',
2340                          'groups' => [],
2341                          'jointype' => filter::JOINTYPE_NONE,
2342                          'count' => 0,
2343                          'expectedusers' => ['exception'],
2344                      ],
2345                      'NONE: Filter on a single group, user in one group' => (object) [
2346                          'loginuser' => 'a',
2347                          'groups' => ['groupa'],
2348                          'jointype' => filter::JOINTYPE_NONE,
2349                          'count' => 0,
2350                          'expectedusers' => [],
2351                      ],
2352                      'NONE: Filter on a single group, user in multple groups' => (object) [
2353                          'loginuser' => 'c',
2354                          'groups' => ['groupa'],
2355                          'jointype' => filter::JOINTYPE_NONE,
2356                          'count' => 1,
2357                          'expectedusers' => [
2358                              'b',
2359                          ],
2360                      ],
2361                      'NONE: Filter on a single group, user in no groups' => (object) [
2362                          'loginuser' => 'd',
2363                          'groups' => ['groupa'],
2364                          'jointype' => filter::JOINTYPE_NONE,
2365                          'count' => 0,
2366                          'expectedusers' => ['exception'],
2367                      ],
2368                      'NONE: Filter on multiple groups, user in one group (ignore invalid groups)' => (object) [
2369                          'loginuser' => 'a',
2370                          'groups' => ['groupa', 'groupb'],
2371                          'jointype' => filter::JOINTYPE_NONE,
2372                          'count' => 0,
2373                          'expectedusers' => [],
2374                      ],
2375                      'NONE: Filter on multiple groups, user in multiple groups' => (object) [
2376                          'loginuser' => 'c',
2377                          'groups' => ['groupa', 'groupb'],
2378                          'jointype' => filter::JOINTYPE_NONE,
2379                          'count' => 0,
2380                          'expectedusers' => [],
2381                      ],
2382                      'NONE: Filter on multiple groups or no groups, user in multiple groups (ignore no groups)' => (object) [
2383                          'loginuser' => 'c',
2384                          'groups' => ['groupa', 'groupb', 'nogroups'],
2385                          'jointype' => filter::JOINTYPE_NONE,
2386                          'count' => 0,
2387                          'expectedusers' => [],
2388                      ],
2389                  ],
2390              ],
2391          ];
2392  
2393          $finaltests = [];
2394          foreach ($tests as $testname => $testdata) {
2395              foreach ($testdata->expect as $expectname => $expectdata) {
2396                  $finaltests["{$testname} => {$expectname}"] = [
2397                      'users' => $testdata->users,
2398                      'groupsavailable' => $testdata->groupsavailable,
2399                      'filtergroups' => $expectdata->groups,
2400                      'jointype' => $expectdata->jointype,
2401                      'count' => $expectdata->count,
2402                      'expectedusers' => $expectdata->expectedusers,
2403                      'loginusername' => $expectdata->loginuser,
2404                  ];
2405              }
2406          }
2407  
2408          return $finaltests;
2409      }
2410  
2411  
2412      /**
2413       * Ensure that the last access filter works as expected with the provided test cases.
2414       *
2415       * @param array $usersdata The list of users to create
2416       * @param array $accesssince The last access data to filter by
2417       * @param int $jointype The join type to use when combining filter values
2418       * @param int $count The expected count
2419       * @param array $expectedusers
2420       * @dataProvider accesssince_provider
2421       */
2422      public function test_accesssince_filter(array $usersdata, array $accesssince, int $jointype, int $count,
2423              array $expectedusers): void {
2424  
2425          $course = $this->getDataGenerator()->create_course();
2426          $coursecontext = context_course::instance($course->id);
2427          $users = [];
2428  
2429          foreach ($usersdata as $username => $userdata) {
2430              $usertimestamp = empty($userdata['lastlogin']) ? 0 : strtotime($userdata['lastlogin']);
2431  
2432              $user = $this->getDataGenerator()->create_user(['username' => $username]);
2433              $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
2434  
2435              // Create the record of the user's last access to the course.
2436              if ($usertimestamp > 0) {
2437                  $this->getDataGenerator()->create_user_course_lastaccess($user, $course, $usertimestamp);
2438              }
2439  
2440              $users[$username] = $user;
2441          }
2442  
2443          // Create a secondary course with users. We should not see these users.
2444          $this->create_course_with_users(1, 1, 1, 1);
2445  
2446          // Create the basic filter.
2447          $filterset = new participants_filterset();
2448          $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id]));
2449  
2450          // Create the last access filter.
2451          $lastaccessfilter = new integer_filter('accesssince');
2452          $filterset->add_filter($lastaccessfilter);
2453  
2454          // Configure the filter.
2455          foreach ($accesssince as $accessstring) {
2456              $lastaccessfilter->add_filter_value(strtotime($accessstring));
2457          }
2458          $lastaccessfilter->set_join_type($jointype);
2459  
2460          // Run the search.
2461          $search = new participants_search($course, $coursecontext, $filterset);
2462          $rs = $search->get_participants();
2463          $this->assertInstanceOf(moodle_recordset::class, $rs);
2464          $records = $this->convert_recordset_to_array($rs);
2465  
2466          $this->assertCount($count, $records);
2467          $this->assertEquals($count, $search->get_total_participants_count());
2468  
2469          foreach ($expectedusers as $expecteduser) {
2470              $this->assertArrayHasKey($users[$expecteduser]->id, $records);
2471          }
2472      }
2473  
2474      /**
2475       * Data provider for last access filter tests.
2476       *
2477       * @return array
2478       */
2479      public function accesssince_provider(): array {
2480          $tests = [
2481              // Users with different last access times.
2482              'Users in different groups' => (object) [
2483                  'users' => [
2484                      'a' => [
2485                          'lastlogin' => '-3 days',
2486                      ],
2487                      'b' => [
2488                          'lastlogin' => '-2 weeks',
2489                      ],
2490                      'c' => [
2491                          'lastlogin' => '-5 months',
2492                      ],
2493                      'd' => [
2494                          'lastlogin' => '-11 months',
2495                      ],
2496                      'e' => [
2497                          // Never logged in.
2498                          'lastlogin' => '',
2499                      ],
2500                  ],
2501                  'expect' => [
2502                      // Tests for jointype: ANY.
2503                      'ANY: No filter' => (object) [
2504                          'accesssince' => [],
2505                          'jointype' => filter::JOINTYPE_ANY,
2506                          'count' => 5,
2507                          'expectedusers' => [
2508                              'a',
2509                              'b',
2510                              'c',
2511                              'd',
2512                              'e',
2513                          ],
2514                      ],
2515                      'ANY: Filter on last login more than 1 year ago' => (object) [
2516                          'accesssince' => ['-1 year'],
2517                          'jointype' => filter::JOINTYPE_ANY,
2518                          'count' => 1,
2519                          'expectedusers' => [
2520                              'e',
2521                          ],
2522                      ],
2523                      'ANY: Filter on last login more than 6 months ago' => (object) [
2524                          'accesssince' => ['-6 months'],
2525                          'jointype' => filter::JOINTYPE_ANY,
2526                          'count' => 2,
2527                          'expectedusers' => [
2528                              'd',
2529                              'e',
2530                          ],
2531                      ],
2532                      'ANY: Filter on last login more than 3 weeks ago' => (object) [
2533                          'accesssince' => ['-3 weeks'],
2534                          'jointype' => filter::JOINTYPE_ANY,
2535                          'count' => 3,
2536                          'expectedusers' => [
2537                              'c',
2538                              'd',
2539                              'e',
2540                          ],
2541                      ],
2542                      'ANY: Filter on last login more than 5 days ago' => (object) [
2543                          'accesssince' => ['-5 days'],
2544                          'jointype' => filter::JOINTYPE_ANY,
2545                          'count' => 4,
2546                          'expectedusers' => [
2547                              'b',
2548                              'c',
2549                              'd',
2550                              'e',
2551                          ],
2552                      ],
2553                      'ANY: Filter on last login more than 2 days ago' => (object) [
2554                          'accesssince' => ['-2 days'],
2555                          'jointype' => filter::JOINTYPE_ANY,
2556                          'count' => 5,
2557                          'expectedusers' => [
2558                              'a',
2559                              'b',
2560                              'c',
2561                              'd',
2562                              'e',
2563                          ],
2564                      ],
2565  
2566                      // Tests for jointype: ALL.
2567                      'ALL: No filter' => (object) [
2568                          'accesssince' => [],
2569                          'jointype' => filter::JOINTYPE_ALL,
2570                          'count' => 5,
2571                          'expectedusers' => [
2572                              'a',
2573                              'b',
2574                              'c',
2575                              'd',
2576                              'e',
2577                          ],
2578                      ],
2579                      'ALL: Filter on last login more than 1 year ago' => (object) [
2580                          'accesssince' => ['-1 year'],
2581                          'jointype' => filter::JOINTYPE_ALL,
2582                          'count' => 1,
2583                          'expectedusers' => [
2584                              'e',
2585                          ],
2586                      ],
2587                      'ALL: Filter on last login more than 6 months ago' => (object) [
2588                          'accesssince' => ['-6 months'],
2589                          'jointype' => filter::JOINTYPE_ALL,
2590                          'count' => 2,
2591                          'expectedusers' => [
2592                              'd',
2593                              'e',
2594                          ],
2595                      ],
2596                      'ALL: Filter on last login more than 3 weeks ago' => (object) [
2597                          'accesssince' => ['-3 weeks'],
2598                          'jointype' => filter::JOINTYPE_ALL,
2599                          'count' => 3,
2600                          'expectedusers' => [
2601                              'c',
2602                              'd',
2603                              'e',
2604                          ],
2605                      ],
2606                      'ALL: Filter on last login more than 5 days ago' => (object) [
2607                          'accesssince' => ['-5 days'],
2608                          'jointype' => filter::JOINTYPE_ALL,
2609                          'count' => 4,
2610                          'expectedusers' => [
2611                              'b',
2612                              'c',
2613                              'd',
2614                              'e',
2615                          ],
2616                      ],
2617                      'ALL: Filter on last login more than 2 days ago' => (object) [
2618                          'accesssince' => ['-2 days'],
2619                          'jointype' => filter::JOINTYPE_ALL,
2620                          'count' => 5,
2621                          'expectedusers' => [
2622                              'a',
2623                              'b',
2624                              'c',
2625                              'd',
2626                              'e',
2627                          ],
2628                      ],
2629  
2630                      // Tests for jointype: NONE.
2631                      'NONE: No filter' => (object) [
2632                          'accesssince' => [],
2633                          'jointype' => filter::JOINTYPE_NONE,
2634                          'count' => 5,
2635                          'expectedusers' => [
2636                              'a',
2637                              'b',
2638                              'c',
2639                              'd',
2640                              'e',
2641                          ],
2642                      ],
2643                      'NONE: Filter on last login more than 1 year ago' => (object) [
2644                          'accesssince' => ['-1 year'],
2645                          'jointype' => filter::JOINTYPE_NONE,
2646                          'count' => 4,
2647                          'expectedusers' => [
2648                              'a',
2649                              'b',
2650                              'c',
2651                              'd',
2652                          ],
2653                      ],
2654                      'NONE: Filter on last login more than 6 months ago' => (object) [
2655                          'accesssince' => ['-6 months'],
2656                          'jointype' => filter::JOINTYPE_NONE,
2657                          'count' => 3,
2658                          'expectedusers' => [
2659                              'a',
2660                              'b',
2661                              'c',
2662                          ],
2663                      ],
2664                      'NONE: Filter on last login more than 3 weeks ago' => (object) [
2665                          'accesssince' => ['-3 weeks'],
2666                          'jointype' => filter::JOINTYPE_NONE,
2667                          'count' => 2,
2668                          'expectedusers' => [
2669                              'a',
2670                              'b',
2671                          ],
2672                      ],
2673                      'NONE: Filter on last login more than 5 days ago' => (object) [
2674                          'accesssince' => ['-5 days'],
2675                          'jointype' => filter::JOINTYPE_NONE,
2676                          'count' => 1,
2677                          'expectedusers' => [
2678                              'a',
2679                          ],
2680                      ],
2681                      'NONE: Filter on last login more than 2 days ago' => (object) [
2682                          'accesssince' => ['-2 days'],
2683                          'jointype' => filter::JOINTYPE_NONE,
2684                          'count' => 0,
2685                          'expectedusers' => [],
2686                      ],
2687                  ],
2688              ],
2689          ];
2690  
2691          $finaltests = [];
2692          foreach ($tests as $testname => $testdata) {
2693              foreach ($testdata->expect as $expectname => $expectdata) {
2694                  $finaltests["{$testname} => {$expectname}"] = [
2695                      'users' => $testdata->users,
2696                      'accesssince' => $expectdata->accesssince,
2697                      'jointype' => $expectdata->jointype,
2698                      'count' => $expectdata->count,
2699                      'expectedusers' => $expectdata->expectedusers,
2700                  ];
2701              }
2702          }
2703  
2704          return $finaltests;
2705      }
2706  
2707      /**
2708       * Ensure that the joins between filters in the filterset work as expected with the provided test cases.
2709       *
2710       * @param array $usersdata The list of users to create
2711       * @param array $filterdata The data to filter by
2712       * @param array $groupsavailable The names of groups that should be created in the course
2713       * @param int $jointype The join type to used between each filter being applied
2714       * @param int $count The expected count
2715       * @param array $expectedusers
2716       * @dataProvider filterset_joins_provider
2717       */
2718      public function test_filterset_joins(array $usersdata, array $filterdata, array $groupsavailable, int $jointype, int $count,
2719              array $expectedusers): void {
2720          global $DB;
2721  
2722          // Ensure sufficient capabilities to view all statuses.
2723          $this->setAdminUser();
2724  
2725          // Remove the default role.
2726          set_config('roleid', 0, 'enrol_manual');
2727  
2728          $course = $this->getDataGenerator()->create_course();
2729          $coursecontext = context_course::instance($course->id);
2730          $roles = $DB->get_records_menu('role', [], '', 'shortname, id');
2731          $users = [];
2732  
2733          // Ensure all enrolment methods are enabled (and mapped where required for filtering later).
2734          $enrolinstances = enrol_get_instances($course->id, false);
2735          $enrolinstancesmap = [];
2736          foreach ($enrolinstances as $instance) {
2737              $plugin = enrol_get_plugin($instance->enrol);
2738              $plugin->update_status($instance, ENROL_INSTANCE_ENABLED);
2739  
2740              $enrolinstancesmap[$instance->enrol] = (int) $instance->id;
2741          }
2742  
2743          // Create the required course groups and mapping.
2744          $nogroupsdata = (object) [
2745              'id' => USERSWITHOUTGROUP,
2746          ];
2747  
2748           $groupsdata = ['nogroups' => $nogroupsdata];
2749          foreach ($groupsavailable as $groupname) {
2750              $groupinfo = [
2751                  'courseid' => $course->id,
2752                  'name' => $groupname,
2753              ];
2754  
2755              $groupsdata[$groupname] = $this->getDataGenerator()->create_group($groupinfo);
2756          }
2757  
2758          // Create test users.
2759          foreach ($usersdata as $username => $userdata) {
2760              $usertimestamp = empty($userdata['lastlogin']) ? 0 : strtotime($userdata['lastlogin']);
2761              unset($userdata['lastlogin']);
2762  
2763              // Prevent randomly generated field values that may cause false fails.
2764              $userdata['firstnamephonetic'] = $userdata['firstnamephonetic'] ?? $userdata['firstname'];
2765              $userdata['lastnamephonetic'] = $userdata['lastnamephonetic'] ?? $userdata['lastname'];
2766              $userdata['middlename'] = $userdata['middlename'] ?? '';
2767              $userdata['alternatename'] = $userdata['alternatename'] ?? $username;
2768  
2769              $user = $this->getDataGenerator()->create_user($userdata);
2770  
2771              foreach ($userdata['enrolments'] as $details) {
2772                  $this->getDataGenerator()->enrol_user($user->id, $course->id, $roles[$details['role']],
2773                          $details['method'], 0, 0, $details['status']);
2774              }
2775  
2776              foreach ($userdata['groups'] as $groupname) {
2777                  $userinfo = [
2778                      'userid' => $user->id,
2779                      'groupid' => (int) $groupsdata[$groupname]->id,
2780                  ];
2781                  $this->getDataGenerator()->create_group_member($userinfo);
2782              }
2783  
2784              if ($usertimestamp > 0) {
2785                  $this->getDataGenerator()->create_user_course_lastaccess($user, $course, $usertimestamp);
2786              }
2787  
2788              $users[$username] = $user;
2789          }
2790  
2791          // Create a secondary course with users. We should not see these users.
2792          $this->create_course_with_users(10, 10, 10, 10);
2793  
2794          // Create the basic filterset.
2795          $filterset = new participants_filterset();
2796          $filterset->set_join_type($jointype);
2797          $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id]));
2798  
2799          // Apply the keywords filter if required.
2800          if (array_key_exists('keywords', $filterdata)) {
2801              $keywordfilter = new string_filter('keywords');
2802              $filterset->add_filter($keywordfilter);
2803  
2804              foreach ($filterdata['keywords']['values'] as $keyword) {
2805                  $keywordfilter->add_filter_value($keyword);
2806              }
2807              $keywordfilter->set_join_type($filterdata['keywords']['jointype']);
2808          }
2809  
2810          // Apply enrolment methods filter if required.
2811          if (array_key_exists('enrolmethods', $filterdata)) {
2812              $enrolmethodfilter = new integer_filter('enrolments');
2813              $filterset->add_filter($enrolmethodfilter);
2814  
2815              foreach ($filterdata['enrolmethods']['values'] as $enrolmethod) {
2816                  $enrolmethodfilter->add_filter_value($enrolinstancesmap[$enrolmethod]);
2817              }
2818              $enrolmethodfilter->set_join_type($filterdata['enrolmethods']['jointype']);
2819          }
2820  
2821          // Apply roles filter if required.
2822          if (array_key_exists('courseroles', $filterdata)) {
2823              $rolefilter = new integer_filter('roles');
2824              $filterset->add_filter($rolefilter);
2825  
2826              foreach ($filterdata['courseroles']['values'] as $rolename) {
2827                  $rolefilter->add_filter_value((int) $roles[$rolename]);
2828              }
2829              $rolefilter->set_join_type($filterdata['courseroles']['jointype']);
2830          }
2831  
2832          // Apply status filter if required.
2833          if (array_key_exists('status', $filterdata)) {
2834              $statusfilter = new integer_filter('status');
2835              $filterset->add_filter($statusfilter);
2836  
2837              foreach ($filterdata['status']['values'] as $status) {
2838                  $statusfilter->add_filter_value($status);
2839              }
2840              $statusfilter->set_join_type($filterdata['status']['jointype']);
2841          }
2842  
2843          // Apply groups filter if required.
2844          if (array_key_exists('groups', $filterdata)) {
2845              $groupsfilter = new integer_filter('groups');
2846              $filterset->add_filter($groupsfilter);
2847  
2848              foreach ($filterdata['groups']['values'] as $filtergroupname) {
2849                  $groupsfilter->add_filter_value((int) $groupsdata[$filtergroupname]->id);
2850              }
2851              $groupsfilter->set_join_type($filterdata['groups']['jointype']);
2852          }
2853  
2854          // Apply last access filter if required.
2855          if (array_key_exists('accesssince', $filterdata)) {
2856              $lastaccessfilter = new integer_filter('accesssince');
2857              $filterset->add_filter($lastaccessfilter);
2858  
2859              foreach ($filterdata['accesssince']['values'] as $accessstring) {
2860                  $lastaccessfilter->add_filter_value(strtotime($accessstring));
2861              }
2862              $lastaccessfilter->set_join_type($filterdata['accesssince']['jointype']);
2863          }
2864  
2865          // Run the search.
2866          $search = new participants_search($course, $coursecontext, $filterset);
2867          $rs = $search->get_participants();
2868          $this->assertInstanceOf(moodle_recordset::class, $rs);
2869          $records = $this->convert_recordset_to_array($rs);
2870  
2871          $this->assertCount($count, $records);
2872          $this->assertEquals($count, $search->get_total_participants_count());
2873  
2874          foreach ($expectedusers as $expecteduser) {
2875              $this->assertArrayHasKey($users[$expecteduser]->id, $records);
2876          }
2877      }
2878  
2879      /**
2880       * Data provider for filterset join tests.
2881       *
2882       * @return array
2883       */
2884      public function filterset_joins_provider(): array {
2885          $tests = [
2886              // Users with different configurations.
2887              'Users with different configurations' => (object) [
2888                  'groupsavailable' => [
2889                      'groupa',
2890                      'groupb',
2891                      'groupc',
2892                  ],
2893                  'users' => [
2894                      'adam.ant' => [
2895                          'firstname' => 'Adam',
2896                          'lastname' => 'Ant',
2897                          'enrolments' => [
2898                              [
2899                                  'role' => 'student',
2900                                  'method' => 'manual',
2901                                  'status' => ENROL_USER_ACTIVE,
2902                              ],
2903                          ],
2904                          'groups' => ['groupa'],
2905                          'lastlogin' => '-3 days',
2906                      ],
2907                      'barbara.bennett' => [
2908                          'firstname' => 'Barbara',
2909                          'lastname' => 'Bennett',
2910                          'enrolments' => [
2911                              [
2912                                  'role' => 'student',
2913                                  'method' => 'manual',
2914                                  'status' => ENROL_USER_ACTIVE,
2915                              ],
2916                              [
2917                                  'role' => 'teacher',
2918                                  'method' => 'manual',
2919                                  'status' => ENROL_USER_ACTIVE,
2920                              ],
2921                          ],
2922                          'groups' => ['groupb'],
2923                          'lastlogin' => '-2 weeks',
2924                      ],
2925                      'colin.carnforth' => [
2926                          'firstname' => 'Colin',
2927                          'lastname' => 'Carnforth',
2928                          'enrolments' => [
2929                              [
2930                                  'role' => 'editingteacher',
2931                                  'method' => 'self',
2932                                  'status' => ENROL_USER_SUSPENDED,
2933                              ],
2934                          ],
2935                          'groups' => ['groupa', 'groupb'],
2936                          'lastlogin' => '-5 months',
2937                      ],
2938                      'tony.rogers' => [
2939                          'firstname' => 'Anthony',
2940                          'lastname' => 'Rogers',
2941                          'enrolments' => [
2942                              [
2943                                  'role' => 'editingteacher',
2944                                  'method' => 'self',
2945                                  'status' => ENROL_USER_SUSPENDED,
2946                              ],
2947                          ],
2948                          'groups' => [],
2949                          'lastlogin' => '-10 months',
2950                      ],
2951                      'sarah.rester' => [
2952                          'firstname' => 'Sarah',
2953                          'lastname' => 'Rester',
2954                          'email' => 'zazu@example.com',
2955                          'enrolments' => [
2956                              [
2957                                  'role' => 'teacher',
2958                                  'method' => 'manual',
2959                                  'status' => ENROL_USER_ACTIVE,
2960                              ],
2961                              [
2962                                  'role' => 'editingteacher',
2963                                  'method' => 'self',
2964                                  'status' => ENROL_USER_SUSPENDED,
2965                              ],
2966                          ],
2967                          'groups' => [],
2968                          'lastlogin' => '-11 months',
2969                      ],
2970                      'morgan.crikeyson' => [
2971                          'firstname' => 'Morgan',
2972                          'lastname' => 'Crikeyson',
2973                          'enrolments' => [
2974                              [
2975                                  'role' => 'teacher',
2976                                  'method' => 'manual',
2977                                  'status' => ENROL_USER_ACTIVE,
2978                              ],
2979                          ],
2980                          'groups' => ['groupa'],
2981                          'lastlogin' => '-1 week',
2982                      ],
2983                      'jonathan.bravo' => [
2984                          'firstname' => 'Jonathan',
2985                          'lastname' => 'Bravo',
2986                          'enrolments' => [
2987                              [
2988                                  'role' => 'student',
2989                                  'method' => 'manual',
2990                                  'status' => ENROL_USER_ACTIVE,
2991                              ],
2992                          ],
2993                          'groups' => [],
2994                          // Never logged in.
2995                          'lastlogin' => '',
2996                      ],
2997                  ],
2998                  'expect' => [
2999                      // Tests for jointype: ANY.
3000                      'ANY: No filters in filterset' => (object) [
3001                          'filterdata' => [],
3002                          'jointype' => filter::JOINTYPE_ANY,
3003                          'count' => 7,
3004                          'expectedusers' => [
3005                              'adam.ant',
3006                              'barbara.bennett',
3007                              'colin.carnforth',
3008                              'tony.rogers',
3009                              'sarah.rester',
3010                              'morgan.crikeyson',
3011                              'jonathan.bravo',
3012                          ],
3013                      ],
3014                      'ANY: Filterset containing a single filter type' => (object) [
3015                          'filterdata' => [
3016                              'enrolmethods' => [
3017                                  'values' => ['self'],
3018                                  'jointype' => filter::JOINTYPE_ANY,
3019                              ],
3020                          ],
3021                          'jointype' => filter::JOINTYPE_ANY,
3022                          'count' => 3,
3023                          'expectedusers' => [
3024                              'colin.carnforth',
3025                              'tony.rogers',
3026                              'sarah.rester',
3027                          ],
3028                      ],
3029                      'ANY: Filterset matching all filter types on different users' => (object) [
3030                          'filterdata' => [
3031                              // Match Adam only.
3032                              'keywords' => [
3033                                  'values' => ['adam'],
3034                                  'jointype' => filter::JOINTYPE_ALL,
3035                              ],
3036                              // Match Sarah only.
3037                              'enrolmethods' => [
3038                                  'values' => ['manual', 'self'],
3039                                  'jointype' => filter::JOINTYPE_ALL,
3040                              ],
3041                              // Match Barbara only.
3042                              'courseroles' => [
3043                                  'values' => ['student', 'teacher'],
3044                                  'jointype' => filter::JOINTYPE_ALL,
3045                              ],
3046                              // Match Sarah only.
3047                              'status' => [
3048                                  'values' => [ENROL_USER_ACTIVE, ENROL_USER_SUSPENDED],
3049                                  'jointype' => filter::JOINTYPE_ALL,
3050                              ],
3051                              // Match Colin only.
3052                              'groups' => [
3053                                  'values' => ['groupa', 'groupb'],
3054                                  'jointype' => filter::JOINTYPE_ALL,
3055                              ],
3056                              // Match Jonathan only.
3057                              'accesssince' => [
3058                                  'values' => ['-1 year'],
3059                                  'jointype' => filter::JOINTYPE_ALL,
3060                                  ],
3061                          ],
3062                          'jointype' => filter::JOINTYPE_ANY,
3063                          'count' => 5,
3064                          // Morgan and Tony are not matched, to confirm filtering is not just returning all users.
3065                          'expectedusers' => [
3066                              'adam.ant',
3067                              'barbara.bennett',
3068                              'colin.carnforth',
3069                              'sarah.rester',
3070                              'jonathan.bravo',
3071                          ],
3072                      ],
3073  
3074                      // Tests for jointype: ALL.
3075                      'ALL: No filters in filterset' => (object) [
3076                          'filterdata' => [],
3077                          'jointype' => filter::JOINTYPE_ALL,
3078                          'count' => 7,
3079                          'expectedusers' => [
3080                              'adam.ant',
3081                              'barbara.bennett',
3082                              'colin.carnforth',
3083                              'tony.rogers',
3084                              'sarah.rester',
3085                              'morgan.crikeyson',
3086                              'jonathan.bravo',
3087                          ],
3088                      ],
3089                      'ALL: Filterset containing a single filter type' => (object) [
3090                          'filterdata' => [
3091                              'enrolmethods' => [
3092                                  'values' => ['self'],
3093                                  'jointype' => filter::JOINTYPE_ANY,
3094                              ],
3095                          ],
3096                          'jointype' => filter::JOINTYPE_ALL,
3097                          'count' => 3,
3098                          'expectedusers' => [
3099                              'colin.carnforth',
3100                              'tony.rogers',
3101                              'sarah.rester',
3102                          ],
3103                      ],
3104                      'ALL: Filterset combining all filter types' => (object) [
3105                          'filterdata' => [
3106                              // Exclude Adam, Tony, Morgan and Jonathan.
3107                              'keywords' => [
3108                                  'values' => ['ar'],
3109                                  'jointype' => filter::JOINTYPE_ANY,
3110                              ],
3111                              // Exclude Colin and Tony.
3112                              'enrolmethods' => [
3113                                  'values' => ['manual'],
3114                                  'jointype' => filter::JOINTYPE_ANY,
3115                              ],
3116                              // Exclude Adam, Barbara and Jonathan.
3117                              'courseroles' => [
3118                                  'values' => ['student'],
3119                                  'jointype' => filter::JOINTYPE_NONE,
3120                              ],
3121                              // Exclude Colin and Tony.
3122                              'status' => [
3123                                  'values' => [ENROL_USER_ACTIVE],
3124                                  'jointype' => filter::JOINTYPE_ALL,
3125                              ],
3126                              // Exclude Barbara.
3127                              'groups' => [
3128                                  'values' => ['groupa', 'nogroups'],
3129                                  'jointype' => filter::JOINTYPE_ANY,
3130                              ],
3131                              // Exclude Adam, Colin and Barbara.
3132                              'accesssince' => [
3133                                  'values' => ['-6 months'],
3134                                  'jointype' => filter::JOINTYPE_ALL,
3135                                  ],
3136                          ],
3137                          'jointype' => filter::JOINTYPE_ALL,
3138                          'count' => 1,
3139                          'expectedusers' => [
3140                              'sarah.rester',
3141                          ],
3142                      ],
3143  
3144                      // Tests for jointype: NONE.
3145                      'NONE: No filters in filterset' => (object) [
3146                          'filterdata' => [],
3147                          'jointype' => filter::JOINTYPE_NONE,
3148                          'count' => 7,
3149                          'expectedusers' => [
3150                              'adam.ant',
3151                              'barbara.bennett',
3152                              'colin.carnforth',
3153                              'tony.rogers',
3154                              'sarah.rester',
3155                              'morgan.crikeyson',
3156                              'jonathan.bravo',
3157                          ],
3158                      ],
3159                      'NONE: Filterset containing a single filter type' => (object) [
3160                          'filterdata' => [
3161                              'enrolmethods' => [
3162                                  'values' => ['self'],
3163                                  'jointype' => filter::JOINTYPE_ANY,
3164                              ],
3165                          ],
3166                          'jointype' => filter::JOINTYPE_NONE,
3167                          'count' => 4,
3168                          'expectedusers' => [
3169                              'adam.ant',
3170                              'barbara.bennett',
3171                              'morgan.crikeyson',
3172                              'jonathan.bravo',
3173                          ],
3174                      ],
3175                      'NONE: Filterset combining all filter types' => (object) [
3176                          'filterdata' => [
3177                              // Excludes Adam.
3178                              'keywords' => [
3179                                  'values' => ['adam'],
3180                                  'jointype' => filter::JOINTYPE_ANY,
3181                              ],
3182                              // Excludes Colin, Tony and Sarah.
3183                              'enrolmethods' => [
3184                                  'values' => ['self'],
3185                                  'jointype' => filter::JOINTYPE_ANY,
3186                              ],
3187                              // Excludes Jonathan.
3188                              'courseroles' => [
3189                                  'values' => ['student'],
3190                                  'jointype' => filter::JOINTYPE_NONE,
3191                              ],
3192                              // Excludes Colin, Tony and Sarah.
3193                              'status' => [
3194                                  'values' => [ENROL_USER_SUSPENDED],
3195                                  'jointype' => filter::JOINTYPE_ALL,
3196                              ],
3197                              // Excludes Adam, Colin, Tony, Sarah, Morgan and Jonathan.
3198                              'groups' => [
3199                                  'values' => ['groupa', 'nogroups'],
3200                                  'jointype' => filter::JOINTYPE_ANY,
3201                              ],
3202                              // Excludes Tony and Sarah.
3203                              'accesssince' => [
3204                                  'values' => ['-6 months'],
3205                                  'jointype' => filter::JOINTYPE_ALL,
3206                              ],
3207                          ],
3208                          'jointype' => filter::JOINTYPE_NONE,
3209                          'count' => 1,
3210                          'expectedusers' => [
3211                              'barbara.bennett',
3212                          ],
3213                      ],
3214                      'NONE: Filterset combining several filter types and a double-negative on keyword' => (object) [
3215                          'jointype' => filter::JOINTYPE_NONE,
3216                          'filterdata' => [
3217                              // Note: This is a jointype NONE on the parent jointype NONE.
3218                              // The result therefore negated in this instance.
3219                              // Include Adam and Anthony.
3220                              'keywords' => [
3221                                  'values' => ['ant'],
3222                                  'jointype' => filter::JOINTYPE_NONE,
3223                              ],
3224                              // Excludes Tony.
3225                              'status' => [
3226                                  'values' => [ENROL_USER_SUSPENDED],
3227                                  'jointype' => filter::JOINTYPE_ALL,
3228                              ],
3229                          ],
3230                          'count' => 1,
3231                          'expectedusers' => [
3232                              'adam.ant',
3233                          ],
3234                      ],
3235                  ],
3236              ],
3237          ];
3238  
3239          $finaltests = [];
3240          foreach ($tests as $testname => $testdata) {
3241              foreach ($testdata->expect as $expectname => $expectdata) {
3242                  $finaltests["{$testname} => {$expectname}"] = [
3243                      'users' => $testdata->users,
3244                      'filterdata' => $expectdata->filterdata,
3245                      'groupsavailable' => $testdata->groupsavailable,
3246                      'jointype' => $expectdata->jointype,
3247                      'count' => $expectdata->count,
3248                      'expectedusers' => $expectdata->expectedusers,
3249                  ];
3250              }
3251          }
3252  
3253          return $finaltests;
3254      }
3255  }