From 4098af1c5235889c50dd98efcb9e64f6e7cd6cbf Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Mon, 28 Jun 2021 20:40:28 +0100 Subject: [PATCH] MDL-26171 user: implement method for retrieving fullname via SQL. --- user/classes/fields.php | 61 ++++++++++++++++ user/classes/table/participants_search.php | 6 +- user/tests/fields_test.php | 73 +++++++++++++++++++ user/tests/table/participants_search_test.php | 8 ++ user/upgrade.txt | 1 + 5 files changed, 148 insertions(+), 1 deletion(-) diff --git a/user/classes/fields.php b/user/classes/fields.php index f5ac4e7676e..92fc460ea1e 100644 --- a/user/classes/fields.php +++ b/user/classes/fields.php @@ -16,6 +16,8 @@ namespace core_user; +use core_text; + /** * Class for retrieving information about user fields that are needed for displaying user identity. * @@ -571,6 +573,65 @@ class fields { 'mappings' => $mappings]; } + /** + * Similar to {@see \moodle_database::sql_fullname} except it returns all user name fields as defined by site config, in a + * single select statement suitable for inclusion in a query/filter for a users fullname, e.g. + * + * [$select, $params] = fields::get_sql_fullname('u'); + * $users = $DB->get_records_sql_menu("SELECT u.id, {$select} FROM {user} u", $params); + * + * @param string|null $tablealias User table alias, if set elsewhere in the query, null if not required + * @param bool $override If true then the alternativefullnameformat format rather than fullnamedisplay format will be used + * @return array SQL select snippet and parameters + */ + public static function get_sql_fullname(?string $tablealias = 'u', bool $override = false): array { + global $DB; + + $unique = self::$uniqueidentifier++; + + $namefields = self::get_name_fields(); + + // Create a dummy user object containing all name fields. + $dummyuser = (object) array_combine($namefields, $namefields); + $dummyfullname = fullname($dummyuser, $override); + + // Extract any name fields from the fullname format in the order that they appear. + $matchednames = array_values(order_in_string($namefields, $dummyfullname)); + $namelookup = $namepattern = $elements = $params = []; + + foreach ($namefields as $index => $namefield) { + $namefieldwithalias = $tablealias ? "{$tablealias}.{$namefield}" : $namefield; + + // Coalesce the name fields to ensure we don't return null. + $emptyparam = "uf{$unique}ep_{$index}"; + $namelookup[$namefield] = "COALESCE({$namefieldwithalias}, :{$emptyparam})"; + $params[$emptyparam] = ''; + + $namepattern[] = '\b' . preg_quote($namefield) . '\b'; + } + + // Grab any content between the name fields, inserting them after each name field. + $chunks = preg_split('/(' . implode('|', $namepattern) . ')/', $dummyfullname); + foreach ($chunks as $index => $chunk) { + if ($index > 0) { + $elements[] = $namelookup[$matchednames[$index - 1]]; + } + + if (core_text::strlen($chunk) > 0) { + // If content is just whitespace, add to elements directly (also Oracle doesn't support passing ' ' as param). + if (preg_match('/^\s+$/', $chunk)) { + $elements[] = "'$chunk'"; + } else { + $elementparam = "uf{$unique}fp_{$index}"; + $elements[] = ":{$elementparam}"; + $params[$elementparam] = $chunk; + } + } + } + + return [$DB->sql_concat(...$elements), $params]; + } + /** * Gets the display name of a given user field. * diff --git a/user/classes/table/participants_search.php b/user/classes/table/participants_search.php index 552e54d6849..b407e51ac94 100644 --- a/user/classes/table/participants_search.php +++ b/user/classes/table/participants_search.php @@ -960,6 +960,8 @@ class participants_search { $keywords = $keywordsfilter->get_filter_values(); } + $canviewfullnames = has_capability('moodle/site:viewfullnames', $this->context); + foreach ($keywords as $index => $keyword) { $searchkey1 = 'search' . $index . '1'; $searchkey2 = 'search' . $index . '2'; @@ -970,9 +972,11 @@ class participants_search { $searchkey7 = 'search' . $index . '7'; $conditions = []; + // Search by fullname. - $fullname = $DB->sql_fullname('u.firstname', 'u.lastname'); + [$fullname, $fullnameparams] = fields::get_sql_fullname('u', $canviewfullnames); $conditions[] = $DB->sql_like($fullname, ':' . $searchkey1, false, false); + $params = array_merge($params, $fullnameparams); // Search by email. $email = $DB->sql_like('email', ':' . $searchkey2, false, false); diff --git a/user/tests/fields_test.php b/user/tests/fields_test.php index 6a36e38a8ad..bd4be5649d8 100644 --- a/user/tests/fields_test.php +++ b/user/tests/fields_test.php @@ -521,4 +521,77 @@ class fields_test extends \advanced_testcase { $selects = $fields->get_sql()->selects; $this->assertEquals(', id, city', $selects); } + + /** + * Data provider for {@see test_get_sql_fullname} + * + * @return array + */ + public function get_sql_fullname_provider(): array { + return [ + ['firstname lastname', 'FN LN'], + ['lastname, firstname', 'LN, FN'], + ['alternatename \'middlename\' lastname!', 'AN \'MN\' LN!'], + ['[firstname lastname alternatename]', '[FN LN AN]'], + ['firstnamephonetic lastnamephonetic', 'FNP LNP'], + ['firstname alternatename lastname', 'FN AN LN'], + ]; + } + + /** + * Test sql_fullname_display method with various fullname formats + * + * @param string $fullnamedisplay + * @param string $expectedfullname + * + * @dataProvider get_sql_fullname_provider + */ + public function test_get_sql_fullname(string $fullnamedisplay, string $expectedfullname): void { + global $DB; + + $this->resetAfterTest(); + + set_config('fullnamedisplay', $fullnamedisplay); + $user = $this->getDataGenerator()->create_user([ + 'firstname' => 'FN', + 'lastname' => 'LN', + 'firstnamephonetic' => 'FNP', + 'lastnamephonetic' => 'LNP', + 'middlename' => 'MN', + 'alternatename' => 'AN', + ]); + + [$sqlfullname, $params] = fields::get_sql_fullname('u'); + $fullname = $DB->get_field_sql("SELECT {$sqlfullname} FROM {user} u WHERE u.id = :id", $params + [ + 'id' => $user->id, + ]); + + $this->assertEquals($expectedfullname, $fullname); + } + + /** + * Test sql_fullname_display when one of the configured name fields is null + */ + public function test_get_sql_fullname_null_field(): void { + global $DB; + + $this->resetAfterTest(); + + set_config('fullnamedisplay', 'firstname lastname alternatename'); + $user = $this->getDataGenerator()->create_user([ + 'firstname' => 'FN', + 'lastname' => 'LN', + ]); + + // Set alternatename field to null, ensure we still get result in later assertion. + $user->alternatename = null; + user_update_user($user, false); + + [$sqlfullname, $params] = fields::get_sql_fullname('u'); + $fullname = $DB->get_field_sql("SELECT {$sqlfullname} FROM {user} u WHERE u.id = :id", $params + [ + 'id' => $user->id, + ]); + + $this->assertEquals('FN LN ', $fullname); + } } diff --git a/user/tests/table/participants_search_test.php b/user/tests/table/participants_search_test.php index b9a2b5fb75b..5d3a96b6d6b 100644 --- a/user/tests/table/participants_search_test.php +++ b/user/tests/table/participants_search_test.php @@ -1073,6 +1073,14 @@ class participants_search_test extends advanced_testcase { 'tony.rogers', ], ], + 'ANY: Filter on fullname only' => (object) [ + 'keywords' => ['Barbara Bennett'], + 'jointype' => filter::JOINTYPE_ANY, + 'count' => 1, + 'expectedusers' => [ + 'barbara.bennett', + ], + ], 'ANY: Filter on middlename only' => (object) [ 'keywords' => ['Jeff'], 'jointype' => filter::JOINTYPE_ANY, diff --git a/user/upgrade.txt b/user/upgrade.txt index 51d0ee8f5de..00bc7f92240 100644 --- a/user/upgrade.txt +++ b/user/upgrade.txt @@ -6,6 +6,7 @@ This files describes API changes for code that uses the user API. update failed all users in the operation would fail. * External function core_user_external::update_users() now returns an error code and message to why a user update action failed. +* New method `core_user\fields::get_sql_fullname` for retrieving user fullname format in SQL statement === 3.11 ===