diff --git a/user/amd/build/local/participantsfilter/filtertypes/country.min.js b/user/amd/build/local/participantsfilter/filtertypes/country.min.js
new file mode 100644
index 00000000000..982e4fb0812
--- /dev/null
+++ b/user/amd/build/local/participantsfilter/filtertypes/country.min.js
@@ -0,0 +1,2 @@
+define ("core_user/local/participantsfilter/filtertypes/country",["exports","../filter"],function(a,b){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;b=function(a){return a&&a.__esModule?a:{default:a}}(b);function c(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){c=function(a){return typeof a}}else{c=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return c(a)}function d(a,b){if(!(a instanceof b)){throw new TypeError("Cannot call a class as a function")}}function e(a,b){for(var c=0,d;c.\n\n/**\n * Country filter\n *\n * @module core_user/local/participantsfilter/filtertypes/country\n * @package core_user\n * @copyright 2021 Paul Holden \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Filter from '../filter';\n\nexport default class extends Filter {\n\n /**\n * For country the final value is an array of country code strings\n *\n * @return {Object}\n */\n get values() {\n return this.rawValues;\n }\n}\n"],"file":"country.min.js"}
\ No newline at end of file
diff --git a/user/amd/src/local/participantsfilter/filtertypes/country.js b/user/amd/src/local/participantsfilter/filtertypes/country.js
new file mode 100644
index 00000000000..2c60065435b
--- /dev/null
+++ b/user/amd/src/local/participantsfilter/filtertypes/country.js
@@ -0,0 +1,37 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see .
+
+/**
+ * Country filter
+ *
+ * @module core_user/local/participantsfilter/filtertypes/country
+ * @package core_user
+ * @copyright 2021 Paul Holden
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import Filter from '../filter';
+
+export default class extends Filter {
+
+ /**
+ * For country the final value is an array of country code strings
+ *
+ * @return {Object}
+ */
+ get values() {
+ return this.rawValues;
+ }
+}
diff --git a/user/classes/output/participants_filter.php b/user/classes/output/participants_filter.php
index 31406cd53e9..895f095e338 100644
--- a/user/classes/output/participants_filter.php
+++ b/user/classes/output/participants_filter.php
@@ -24,6 +24,7 @@
namespace core_user\output;
use context_course;
+use core\user_fields;
use renderable;
use renderer_base;
use stdClass;
@@ -89,6 +90,10 @@ class participants_filter implements renderable, templatable {
$filtertypes[] = $filtertype;
}
+ if ($filtertype = $this->get_country_filter()) {
+ $filtertypes[] = $filtertype;
+ }
+
return $filtertypes;
}
@@ -322,6 +327,34 @@ class participants_filter implements renderable, templatable {
);
}
+ /**
+ * Get data for the country filter
+ *
+ * @return stdClass|null
+ */
+ protected function get_country_filter(): ?stdClass {
+ $extrauserfields = user_fields::get_identity_fields($this->context, false);
+ if (array_search('country', $extrauserfields) === false) {
+ return null;
+ }
+
+ $countries = get_string_manager()->get_list_of_countries(true);
+
+ return $this->get_filter_object(
+ 'country',
+ get_string('country'),
+ false,
+ true,
+ 'core_user/local/participantsfilter/filtertypes/country',
+ array_map(function(string $code, string $name): stdClass {
+ return (object) [
+ 'value' => $code,
+ 'title' => $name,
+ ];
+ }, array_keys($countries), array_values($countries))
+ );
+ }
+
/**
* Get data for the keywords filter.
*
diff --git a/user/classes/table/participants_filterset.php b/user/classes/table/participants_filterset.php
index e0d0efe0658..a94b9cf9459 100644
--- a/user/classes/table/participants_filterset.php
+++ b/user/classes/table/participants_filterset.php
@@ -27,7 +27,6 @@ declare(strict_types=1);
namespace core_user\table;
-use core_table\local\filter\boolean_filter;
use core_table\local\filter\filterset;
use core_table\local\filter\integer_filter;
use core_table\local\filter\string_filter;
@@ -61,6 +60,7 @@ class participants_filterset extends filterset {
* - enrolments;
* - groups;
* - keywords;
+ * - country;
* - roles; and
* - status.
*
@@ -72,6 +72,7 @@ class participants_filterset extends filterset {
'enrolments' => integer_filter::class,
'groups' => integer_filter::class,
'keywords' => string_filter::class,
+ 'country' => string_filter::class,
'roles' => integer_filter::class,
'status' => integer_filter::class,
];
diff --git a/user/classes/table/participants_search.php b/user/classes/table/participants_search.php
index 754c42ec0ba..e931d89290e 100644
--- a/user/classes/table/participants_search.php
+++ b/user/classes/table/participants_search.php
@@ -257,6 +257,22 @@ class participants_search {
}
}
+ // Apply any country filtering.
+ if ($this->filterset->has_filter('country')) {
+ [
+ 'where' => $countrywhere,
+ 'params' => $countryparams,
+ ] = $this->get_country_sql();
+
+ if (!empty($countrywhere)) {
+ $wheres[] = "($countrywhere)";
+ }
+
+ if (!empty($countryparams)) {
+ $params = array_merge($params, $countryparams);
+ }
+ }
+
// Apply any keyword text searches.
if ($this->filterset->has_filter('keywords')) {
[
@@ -877,6 +893,32 @@ class participants_search {
];
}
+ /**
+ * Prepare SQL where clause and associated parameters for country filtering
+ *
+ * @return array SQL query data in the format ['where' => '', 'params' => []].
+ */
+ protected function get_country_sql(): array {
+ global $DB;
+
+ $where = '';
+ $params = [];
+
+ $countryfilter = $this->filterset->get_filter('country');
+ if ($countrycodes = $countryfilter->get_filter_values()) {
+ // If filter type is "None", then we negate the comparison.
+ [$countrywhere, $params] = $DB->get_in_or_equal($countrycodes, SQL_PARAMS_NAMED, 'country',
+ $countryfilter->get_join_type() !== $countryfilter::JOINTYPE_NONE);
+
+ $where = "(u.country {$countrywhere})";
+ }
+
+ return [
+ 'where' => $where,
+ 'params' => $params,
+ ];
+ }
+
/**
* Prepare SQL where clause and associated parameters for any keyword searches being performed.
*
@@ -975,7 +1017,7 @@ class participants_search {
$extrasearchfields = user_fields::get_identity_fields(null);
foreach ($extrasearchfields as $fieldindex => $extrasearchfield) {
if (in_array($extrasearchfield, ['email', 'idnumber', 'country'])) {
- // Already covered above. Search by country not supported.
+ // Already covered above.
continue;
}
// The param must be short (max 32 characters) so don't include field name.
diff --git a/user/tests/table/participants_search_test.php b/user/tests/table/participants_search_test.php
index c95cd085845..b9a2b5fb75b 100644
--- a/user/tests/table/participants_search_test.php
+++ b/user/tests/table/participants_search_test.php
@@ -757,6 +757,172 @@ class participants_search_test extends advanced_testcase {
return $finaltests;
}
+ /**
+ * Test participant search country filter
+ *
+ * @param array $usersdata
+ * @param array $countries
+ * @param int $jointype
+ * @param array $expectedusers
+ *
+ * @dataProvider country_provider
+ */
+ public function test_country_filter(array $usersdata, array $countries, int $jointype, array $expectedusers): void {
+ $this->resetAfterTest();
+
+ $course = $this->getDataGenerator()->create_course();
+ $users = [];
+
+ foreach ($usersdata as $username => $country) {
+ $users[$username] = $this->getDataGenerator()->create_and_enrol($course, 'student', (object) [
+ 'username' => $username,
+ 'country' => $country,
+ ]);
+ }
+
+ // Add filters (courseid is required).
+ $filterset = new participants_filterset();
+ $filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id]));
+ $filterset->add_filter(new string_filter('country', $jointype, $countries));
+
+ // Run the search, assert count matches the number of expected users.
+ $search = new participants_search($course, context_course::instance($course->id), $filterset);
+ $this->assertEquals(count($expectedusers), $search->get_total_participants_count());
+
+ $rs = $search->get_participants();
+ $this->assertInstanceOf(moodle_recordset::class, $rs);
+
+ // Assert that each expected user is within the participant records.
+ $records = $this->convert_recordset_to_array($rs);
+ foreach ($expectedusers as $expecteduser) {
+ $this->assertArrayHasKey($users[$expecteduser]->id, $records);
+ }
+ }
+
+ /**
+ * Data provider for {@see test_country_filter}
+ *
+ * @return array
+ */
+ public function country_provider(): array {
+ $tests = [
+ 'users' => [
+ 'user1' => 'DE',
+ 'user2' => 'ES',
+ 'user3' => 'ES',
+ 'user4' => 'GB',
+ ],
+ 'expects' => [
+ // Tests for jointype: ANY.
+ 'ANY: No filter' => (object) [
+ 'countries' => [],
+ 'jointype' => filter::JOINTYPE_ANY,
+ 'expectedusers' => [
+ 'user1',
+ 'user2',
+ 'user3',
+ 'user4',
+ ],
+ ],
+ 'ANY: Matching filters' => (object) [
+ 'countries' => [
+ 'DE',
+ 'GB',
+ ],
+ 'jointype' => filter::JOINTYPE_ANY,
+ 'expectedusers' => [
+ 'user1',
+ 'user4',
+ ],
+ ],
+ 'ANY: Non-matching filters' => (object) [
+ 'countries' => [
+ 'RU',
+ ],
+ 'jointype' => filter::JOINTYPE_ANY,
+ 'expectedusers' => [],
+ ],
+
+ // Tests for jointype: ALL.
+ 'ALL: No filter' => (object) [
+ 'countries' => [],
+ 'jointype' => filter::JOINTYPE_ALL,
+ 'expectedusers' => [
+ 'user1',
+ 'user2',
+ 'user3',
+ 'user4',
+ ],
+ ],
+ 'ALL: Matching filters' => (object) [
+ 'countries' => [
+ 'DE',
+ 'GB',
+ ],
+ 'jointype' => filter::JOINTYPE_ALL,
+ 'expectedusers' => [
+ 'user1',
+ 'user4',
+ ],
+ ],
+ 'ALL: Non-matching filters' => (object) [
+ 'countries' => [
+ 'RU',
+ ],
+ 'jointype' => filter::JOINTYPE_ALL,
+ 'expectedusers' => [],
+ ],
+
+ // Tests for jointype: NONE.
+ 'NONE: No filter' => (object) [
+ 'countries' => [],
+ 'jointype' => filter::JOINTYPE_NONE,
+ 'expectedusers' => [
+ 'user1',
+ 'user2',
+ 'user3',
+ 'user4',
+ ],
+ ],
+ 'NONE: Matching filters' => (object) [
+ 'countries' => [
+ 'DE',
+ 'GB',
+ ],
+ 'jointype' => filter::JOINTYPE_NONE,
+ 'expectedusers' => [
+ 'user2',
+ 'user3',
+ ],
+ ],
+ 'NONE: Non-matching filters' => (object) [
+ 'countries' => [
+ 'RU',
+ ],
+ 'jointype' => filter::JOINTYPE_NONE,
+ 'expectedusers' => [
+ 'user1',
+ 'user2',
+ 'user3',
+ 'user4',
+ ],
+ ],
+ ],
+ ];
+
+ $finaltests = [];
+ foreach ($tests['expects'] as $testname => $test) {
+ $finaltests[$testname] = [
+ 'users' => $tests['users'],
+ 'countries' => $test->countries,
+ 'jointype' => $test->jointype,
+ 'expectedusers' => $test->expectedusers,
+ ];
+ }
+
+ return $finaltests;
+ }
+
/**
* Ensure that the keywords filter works as expected with the provided test cases.
*