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