Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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

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