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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body