MDL-45242 Lib: Allow custom profile fields in showuseridentity

This commit is contained in:
sam marshall 2020-10-12 15:24:07 +01:00
parent 9ddb51b07e
commit 677e1c6248
9 changed files with 1353 additions and 92 deletions

View File

@ -214,21 +214,38 @@ if ($hassiteconfig
// with moodle/site:viewuseridentity).
// Options include fields from the user table that might be helpful to
// distinguish when adding or listing users ('I want to add the John
// Smith from Science faculty').
// Custom user profile fields are not currently supported.
// Smith from Science faculty') and any custom profile fields.
$temp->add(new admin_setting_configmulticheckbox('showuseridentity',
new lang_string('showuseridentity', 'admin'),
new lang_string('showuseridentity_desc', 'admin'), array('email' => 1), array(
'username' => new lang_string('username'),
'idnumber' => new lang_string('idnumber'),
'email' => new lang_string('email'),
'phone1' => new lang_string('phone1'),
'phone2' => new lang_string('phone2'),
'department' => new lang_string('department'),
'institution' => new lang_string('institution'),
'city' => new lang_string('city'),
'country' => new lang_string('country'),
)));
new lang_string('showuseridentity_desc', 'admin'), ['email' => 1],
function() {
global $DB;
// Basic fields available in user table.
$fields = [
'username' => new lang_string('username'),
'idnumber' => new lang_string('idnumber'),
'email' => new lang_string('email'),
'phone1' => new lang_string('phone1'),
'phone2' => new lang_string('phone2'),
'department' => new lang_string('department'),
'institution' => new lang_string('institution'),
'city' => new lang_string('city'),
'country' => new lang_string('country'),
];
// Custom profile fields.
$profilefields = $DB->get_records('user_info_field', ['datatype' => 'text'], 'sortorder ASC');
foreach ($profilefields as $key => $field) {
// Only reasonable-length fields can be used as identity fields.
if ($field->param2 > 255) {
continue;
}
$fields['profile_field_' . $field->shortname] = $field->name . ' *';
}
return $fields;
}));
$setting = new admin_setting_configtext('fullnamedisplay', new lang_string('fullnamedisplay', 'admin'),
new lang_string('configfullnamedisplay', 'admin'), 'language', PARAM_TEXT, 50);
$setting->set_force_ltr(true);

View File

@ -1205,7 +1205,9 @@ $string['setupsearchengine'] = 'Setup search engine';
$string['showcommentscount'] = 'Show comments count';
$string['showdetails'] = 'Show details';
$string['showuseridentity'] = 'Show user identity';
$string['showuseridentity_desc'] = 'When selecting or searching for users, and when displaying lists of users, these fields may be shown in addition to their full name. The fields are only shown to users who have the moodle/site:viewuseridentity capability; by default, teachers and managers. (This option makes most sense if you choose one or two fields that are mandatory at your institution.)';
$string['showuseridentity_desc'] = 'When selecting or searching for users, and when displaying lists of users, these fields may be shown in addition to their full name. The fields are only shown to users who have the moodle/site:viewuseridentity capability; by default, teachers and managers. (This option makes most sense if you choose one or two fields that are mandatory at your institution.)
Fields marked * are custom user profile fields. You can select these fields, but there are currently some screens on which they will not appear.';
$string['simplexmlrequired'] = 'The SimpleXML PHP extension is now required by Moodle.';
$string['sitemenubar'] = 'Site navigation';
$string['sitemailcharset'] = 'Character set';

644
lib/classes/user_fields.php Normal file
View File

@ -0,0 +1,644 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
namespace core;
/**
* Class for retrieving information about user fields that are needed for displaying user identity.
*
* @package core
*/
class user_fields {
/** @var string Prefix used to identify custom profile fields */
const PROFILE_FIELD_PREFIX = 'profile_field_';
/** @var string Regular expression used to match a field name against the prefix */
const PROFILE_FIELD_REGEX = '~^' . self::PROFILE_FIELD_PREFIX . '(.*)$~';
/** @var int All fields required to display user's identity, based on server configuration */
const PURPOSE_IDENTITY = 0;
/** @var int All fields required to display a user picture */
const PURPOSE_USERPIC = 1;
/** @var int All fields required for somebody's name */
const PURPOSE_NAME = 2;
/** @var int Field required by custom include list */
const CUSTOM_INCLUDE = 3;
/** @var \context|null Context in use */
protected $context;
/** @var bool True to allow custom user fields */
protected $allowcustom;
/** @var bool[] Array of purposes (from PURPOSE_xx to true/false) */
protected $purposes;
/** @var string[] List of extra fields to include */
protected $include;
/** @var string[] List of fields to exclude */
protected $exclude;
/** @var int Unique identifier for different queries generated in same request */
protected static $uniqueidentifier = 1;
/** @var array|null Associative array from field => array of purposes it was used for => true */
protected $fields = null;
/**
* Protected constructor - use one of the for_xx methods to create an object.
*
* @param int $purpose Initial purpose for object or -1 for none
*/
protected function __construct(int $purpose = -1) {
$this->purposes = [
self::PURPOSE_IDENTITY => false,
self::PURPOSE_USERPIC => false,
self::PURPOSE_NAME => false,
];
if ($purpose != -1) {
$this->purposes[$purpose] = true;
}
$this->include = [];
$this->exclude = [];
$this->context = null;
$this->allowcustom = true;
}
/**
* Constructs an empty user fields object to get arbitrary user fields.
*
* You can add fields to retrieve with the including() function.
*
* @return user_fields User fields object ready for use
*/
public static function empty(): user_fields {
return new user_fields();
}
/**
* Constructs a user fields object to get identity information for display.
*
* The function does all the required capability checks to see if the current user is allowed
* to see them in the specified context. You can pass context null to get all the fields without
* checking permissions.
*
* If the code can only handle fields in the main user table, and not custom profile fields,
* then set $allowcustom to false.
*
* Note: After constructing the object you can use the ->with_xx, ->including, and ->excluding
* functions to control the required fields in more detail. For example:
*
* $fields = user_fields::for_identity($context)->with_userpic()->excluding('email');
*
* @param \context|null $context Context; if supplied, includes only fields the current user should see
* @param bool $allowcustom If true, custom profile fields may be included
* @return user_fields User fields object ready for use
*/
public static function for_identity(?\context $context, bool $allowcustom = true): user_fields {
$fields = new user_fields(self::PURPOSE_IDENTITY);
$fields->context = $context;
$fields->allowcustom = $allowcustom;
return $fields;
}
/**
* Constructs a user fields object to get information required for displaying a user picture.
*
* Note: After constructing the object you can use the ->with_xx, ->including, and ->excluding
* functions to control the required fields in more detail. For example:
*
* $fields = user_fields::for_userpic()->with_name()->excluding('email');
*
* @return user_fields User fields object ready for use
*/
public static function for_userpic(): user_fields {
return new user_fields(self::PURPOSE_USERPIC);
}
/**
* Constructs a user fields object to get information required for displaying a user full name.
*
* Note: After constructing the object you can use the ->with_xx, ->including, and ->excluding
* functions to control the required fields in more detail. For example:
*
* $fields = user_fields::for_name()->with_userpic()->excluding('email');
*
* @return user_fields User fields object ready for use
*/
public static function for_name(): user_fields {
return new user_fields(self::PURPOSE_NAME);
}
/**
* On an existing user_fields object, adds the fields required for displaying user pictures.
*
* @return $this Same object for chaining function calls
*/
public function with_userpic(): user_fields {
$this->purposes[self::PURPOSE_USERPIC] = true;
return $this;
}
/**
* On an existing user_fields object, adds the fields required for displaying user full names.
*
* @return $this Same object for chaining function calls
*/
public function with_name(): user_fields {
$this->purposes[self::PURPOSE_NAME] = true;
return $this;
}
/**
* On an existing user_fields object, adds the fields required for displaying user identity.
*
* The function does all the required capability checks to see if the current user is allowed
* to see them in the specified context. You can pass context null to get all the fields without
* checking permissions.
*
* If the code can only handle fields in the main user table, and not custom profile fields,
* then set $allowcustom to false.
*
* @param \context|null Context; if supplied, includes only fields the current user should see
* @param bool $allowcustom If true, custom profile fields may be included
* @return $this Same object for chaining function calls
*/
public function with_identity(?\context $context, bool $allowcustom = true): user_fields {
$this->context = $context;
$this->allowcustom = $allowcustom;
$this->purposes[self::PURPOSE_IDENTITY] = true;
return $this;
}
/**
* On an existing user_fields object, adds extra fields to be retrieved. You can specify either
* fields from the user table e.g. 'email', or profile fields e.g. 'profile_field_height'.
*
* @param string ...$include One or more fields to add
* @return $this Same object for chaining function calls
*/
public function including(string ...$include): user_fields {
$this->include = array_merge($this->include, $include);
return $this;
}
/**
* On an existing user_fields object, excludes fields from retrieval. You can specify either
* fields from the user table e.g. 'email', or profile fields e.g. 'profile_field_height'.
*
* This is useful when constructing queries where your query already explicitly references
* certain fields, so you don't want to retrieve them twice.
*
* @param string ...$exclude One or more fields to exclude
* @return $this Same object for chaining function calls
*/
public function excluding(...$exclude): user_fields {
$this->exclude = array_merge($this->exclude, $exclude);
return $this;
}
/**
* Gets an array of all fields that are required for the specified purposes, also taking
* into account the $includes and $excludes settings.
*
* The results may include basic field names (columns from the 'user' database table) and,
* unless turned off, custom profile field names in the format 'profile_field_myfield'.
*
* You should not rely on the order of fields, with one exception: if there is an id field
* it will be returned first. This is in case it is used with get_records calls.
*
* The $limitpurposes parameter is useful if you want to get a different set of fields than the
* purposes in the constructor. For example, if you want to get SQL for identity + user picture
* fields, but you then want to only get the identity fields as a list. (You can only specify
* purposes that were also passed to the constructor i.e. it can only be used to restrict the
* list, not add to it.)
*
* @param array $limitpurposes If specified, gets fields only for these purposes
* @return string[] Array of required fields
* @throws \coding_exception If any unknown purpose is listed
*/
public function get_required_fields(array $limitpurposes = []): array {
// The first time this is called, actually work out the list. There is no way to 'un-cache'
// it, but these objects are designed to be short-lived so it doesn't need one.
if ($this->fields === null) {
// Add all the fields as array keys so that there are no duplicates.
$this->fields = [];
if ($this->purposes[self::PURPOSE_IDENTITY]) {
foreach (self::get_identity_fields($this->context, $this->allowcustom) as $field) {
$this->fields[$field] = [self::PURPOSE_IDENTITY => true];
}
}
if ($this->purposes[self::PURPOSE_USERPIC]) {
foreach (self::get_picture_fields() as $field) {
if (!array_key_exists($field, $this->fields)) {
$this->fields[$field] = [];
}
$this->fields[$field][self::PURPOSE_USERPIC] = true;
}
}
if ($this->purposes[self::PURPOSE_NAME]) {
foreach (self::get_name_fields() as $field) {
if (!array_key_exists($field, $this->fields)) {
$this->fields[$field] = [];
}
$this->fields[$field][self::PURPOSE_NAME] = true;
}
}
foreach ($this->include as $field) {
if ($this->allowcustom || !preg_match(self::PROFILE_FIELD_REGEX, $field)) {
if (!array_key_exists($field, $this->fields)) {
$this->fields[$field] = [];
}
$this->fields[$field][self::CUSTOM_INCLUDE] = true;
}
}
foreach ($this->exclude as $field) {
unset($this->fields[$field]);
}
// If the id field is included, make sure it's first in the list.
if (array_key_exists('id', $this->fields)) {
$newfields = ['id' => $this->fields['id']];
foreach ($this->fields as $field => $purposes) {
if ($field !== 'id') {
$newfields[$field] = $purposes;
}
}
$this->fields = $newfields;
}
}
if ($limitpurposes) {
// Check the value was legitimate.
foreach ($limitpurposes as $purpose) {
if ($purpose != self::CUSTOM_INCLUDE && empty($this->purposes[$purpose])) {
throw new \coding_exception('$limitpurposes can only include purposes defined in object');
}
}
// Filter the fields to include only those matching the purposes.
$result = [];
foreach ($this->fields as $key => $purposes) {
foreach ($limitpurposes as $purpose) {
if (array_key_exists($purpose, $purposes)) {
$result[] = $key;
break;
}
}
}
return $result;
} else {
return array_keys($this->fields);
}
}
/**
* Gets fields required for user pictures.
*
* The results include only basic field names (columns from the 'user' database table).
*
* @return string[] All fields required for user pictures
*/
public static function get_picture_fields(): array {
return ['id', 'picture', 'firstname', 'lastname', 'firstnamephonetic', 'lastnamephonetic',
'middlename', 'alternatename', 'imagealt', 'email'];
}
/**
* Gets fields required for user names.
*
* The results include only basic field names (columns from the 'user' database table).
*
* Fields are usually returned in a specific order, which the fullname() function depends on.
* If you specify 'true' to the $strangeorder flag, then the firstname and lastname fields
* are moved to the front; this is useful in a few places in existing code. New code should
* avoid requiring a particular order.
*
* @param bool $differentorder In a few places, a different order of fields is required
* @return string[] All fields used to display user names
*/
public static function get_name_fields(bool $differentorder = false): array {
$fields = ['firstnamephonetic', 'lastnamephonetic', 'middlename', 'alternatename',
'firstname', 'lastname'];
if ($differentorder) {
return array_merge(array_slice($fields, -2), array_slice($fields, 0, -2));
} else {
return $fields;
}
}
/**
* Gets all fields required for user identity. These fields should be included in tables
* showing lists of users (in addition to the user's name which is included as standard).
*
* The results include basic field names (columns from the 'user' database table) and, unless
* turned off, custom profile field names in the format 'profile_field_myfield'.
*
* This function does all the required capability checks to see if the current user is allowed
* to see them in the specified context. You can pass context null to get all the fields
* without checking permissions.
*
* @param \context|null $context Context; if not supplied, all fields will be included without checks
* @param bool $allowcustom If true, custom profile fields will be included
* @return string[] Array of required fields
* @throws \coding_exception
*/
public static function get_identity_fields(?\context $context, bool $allowcustom = true): array {
global $CFG;
// Only users with permission get the extra fields.
if ($context && !has_capability('moodle/site:viewuseridentity', $context)) {
return [];
}
// Split showuseridentity on comma (filter needed in case the showuseridentity is empty).
$extra = array_filter(explode(',', $CFG->showuseridentity));
// If there are any custom fields, remove them if necessary (either if allowcustom is false,
// or if the user doesn't have access to see them).
foreach ($extra as $key => $field) {
if (preg_match(self::PROFILE_FIELD_REGEX, $field, $matches)) {
if ($allowcustom) {
require_once($CFG->dirroot . '/user/profile/lib.php');
$fieldinfo = profile_get_custom_field_data_by_shortname($matches[1]);
switch ($fieldinfo['visible']) {
case PROFILE_VISIBLE_NONE:
case PROFILE_VISIBLE_PRIVATE:
$allowed = !$context || has_capability('moodle/user:viewalldetails', $context);
break;
case PROFILE_VISIBLE_ALL:
$allowed = true;
break;
}
} else {
$allowed = false;
}
if (!$allowed) {
unset($extra[$key]);
}
}
}
// For standard user fields, access is controlled by the hiddenuserfields option and
// some different capabilities. Check and remove these if the user can't access them.
$hiddenfields = array_filter(explode(',', $CFG->hiddenuserfields));
$hiddenidentifiers = array_intersect($extra, $hiddenfields);
if ($hiddenidentifiers) {
if (!$context) {
$canviewhiddenuserfields = true;
} else if ($context->get_course_context(false)) {
// We are somewhere inside a course.
$canviewhiddenuserfields = has_capability('moodle/course:viewhiddenuserfields', $context);
} else {
// We are not inside a course.
$canviewhiddenuserfields = has_capability('moodle/user:viewhiddendetails', $context);
}
if (!$canviewhiddenuserfields) {
// Remove hidden identifiers from the list.
$extra = array_diff($extra, $hiddenidentifiers);
}
}
// Re-index the entries and return.
$extra = array_values($extra);
return $extra;
}
/**
* Gets SQL that can be used in a query to get the necessary fields.
*
* The result of this function is an object with fields 'selects', 'joins', 'params', and
* 'mappings'.
*
* If not empty, the list of selects will begin with a comma and the list of joins will begin
* and end with a space. You can include the result in your existing query like this:
*
* SELECT (your existing fields)
* $selects
* FROM {user} u
* JOIN (your existing joins)
* $joins
*
* When there are no custom fields then the 'joins' result will always be an empty string, and
* 'params' will be an empty array.
*
* The $fieldmappings value is often not needed. It is an associative array from each field
* name to an SQL expression for the value of that field, e.g.:
* 'profile_field_frog' => 'uf1d_3.data'
* 'city' => 'u.city'
* This is helpful if you want to use the profile fields in a WHERE clause, becuase you can't
* refer to the aliases used in the SELECT list there.
*
* The leading comma is included because this makes it work in the pattern above even if there
* are no fields from the user_fields data (which can happen if doing identity fields and none
* are selected). If you want the result without a leading comma, set $leadingcomma to false.
*
* If the 'id' field is included then it will always be first in the list. Otherwise, you
* should not rely on the field order.
*
* For identity fields, the function does all the required capability checks to see if the
* current user is allowed to see them in the specified context. You can pass context null
* to get all the fields without checking permissions.
*
* If your code for any reason cannot cope with custom fields then you can turn them off.
*
* You can have either named or ? params. If you use named params, they are of the form
* uf1s_2; the first number increments in each call using a static variable in this class and
* the second number refers to the field being queried. A similar pattern is used to make
* join aliases unique.
*
* If your query refers to the user table by an alias e.g. 'u' then specify this in the $alias
* parameter; otherwise it will use {user} (if there are any joins for custom profile fields)
* or simply refer to the field by name only (if there aren't).
*
* If you need to use a prefix on the field names (for example in case they might coincide with
* existing result columns from your query, or if you want a convenient way to split out all
* the user data into a separate object) then you can specify one here. For example, if you
* include name fields and the prefix is 'u_' then the results will include 'u_firstname'.
*
* If you don't want to prefix all the field names but only change the id field name, use
* the $renameid parameter. (When you use this parameter, it takes precedence over any prefix;
* the id field will not be prefixed, while all others will.)
*
* @param string $alias Optional (but recommended) alias for user table in query, e.g. 'u'
* @param bool $namedparams If true, uses named :parameters instead of indexed ? parameters
* @param string $prefix Optional prefix for all field names in result, e.g. 'u_'
* @param string $renameid Renames the 'id' field if specified, e.g. 'userid'
* @param bool $leadingcomma If true the 'selects' list will start with a comma
* @return \stdClass Object with necessary SQL components
*/
public function get_sql(string $alias = '', bool $namedparams = false, string $prefix = '',
string $renameid = '', bool $leadingcomma = true): \stdClass {
global $DB;
$fields = $this->get_required_fields();
$selects = '';
$joins = '';
$params = [];
$mappings = [];
$unique = self::$uniqueidentifier++;
$fieldcount = 0;
if ($alias) {
$usertable = $alias . '.';
} else {
// If there is no alias, we still need to use {user} to identify the table when there
// are joins with other tables. When there are no customfields then there are no joins
// so we can refer to the fields by name alone.
$gotcustomfields = false;
foreach ($fields as $field) {
if (preg_match(self::PROFILE_FIELD_REGEX, $field, $matches)) {
$gotcustomfields = true;
break;
}
}
if ($gotcustomfields) {
$usertable = '{user}.';
} else {
$usertable = '';
}
}
foreach ($fields as $field) {
if (preg_match(self::PROFILE_FIELD_REGEX, $field, $matches)) {
// Custom profile field.
$shortname = $matches[1];
$fieldcount++;
$fieldalias = 'uf' . $unique . 'f_' . $fieldcount;
$dataalias = 'uf' . $unique . 'd_' . $fieldcount;
if ($namedparams) {
$withoutcolon = 'uf' . $unique . 's' . $fieldcount;
$placeholder = ':' . $withoutcolon;
$params[$withoutcolon] = $shortname;
} else {
$placeholder = '?';
$params[] = $shortname;
}
$joins .= " JOIN {user_info_field} $fieldalias ON $fieldalias.shortname = $placeholder
LEFT JOIN {user_info_data} $dataalias ON $dataalias.fieldid = $fieldalias.id
AND $dataalias.userid = {$usertable}id";
// For Oracle we need to convert the field into a usable format.
$fieldsql = $DB->sql_compare_text($dataalias . '.data', 255);
$selects .= ", $fieldsql AS $prefix$field";
$mappings[$field] = $fieldsql;
} else {
// Standard user table field.
$selects .= ", $usertable$field";
if ($field === 'id' && $renameid && $renameid !== 'id') {
$selects .= " AS $renameid";
} else if ($prefix) {
$selects .= " AS $prefix$field";
}
$mappings[$field] = "$usertable$field";
}
}
// Add a space to the end of the joins list; this means it can be appended directly into
// any existing query without worrying about whether the developer has remembered to add
// whitespace after it.
if ($joins) {
$joins .= ' ';
}
// Optionally remove the leading comma.
if (!$leadingcomma) {
$selects = ltrim($selects, ' ,');
}
return (object)['selects' => $selects, 'joins' => $joins, 'params' => $params,
'mappings' => $mappings];
}
/**
* Gets the display name of a given user field.
*
* Supports field names from the 'user' database table, and custom profile fields supplied in
* the format 'profile_field_xx'.
*
* @param string $field Field name in database
* @return string Field name for display to user
* @throws \coding_exception
*/
public static function get_display_name(string $field): string {
global $CFG;
// Custom fields have special handling.
if (preg_match(self::PROFILE_FIELD_REGEX, $field, $matches)) {
require_once($CFG->dirroot . '/user/profile/lib.php');
$fieldinfo = profile_get_custom_field_data_by_shortname($matches[1]);
// Use format_string so it can be translated with multilang filter if necessary.
return format_string($fieldinfo['name']);
}
// Some fields have language strings which are not the same as field name.
switch ($field) {
case 'url' : {
return get_string('webpage');
}
case 'icq' : {
return get_string('icqnumber');
}
case 'skype' : {
return get_string('skypeid');
}
case 'aim' : {
return get_string('aimid');
}
case 'yahoo' : {
return get_string('yahooid');
}
case 'msn' : {
return get_string('msnid');
}
case 'picture' : {
return get_string('pictureofuser');
}
}
// Otherwise just use the same lang string.
return get_string($field);
}
/**
* Resets the unique identifier used to ensure that multiple SQL fragments generated in the
* same request will have different identifiers for parameters and table aliases.
*
* This is intended only for use in unit testing.
*/
public static function reset_unique_identifier() {
self::$uniqueidentifier = 1;
}
/**
* Checks if a field name looks like a custom profile field i.e. it begins with profile_field_
* (does not check if that profile field actually exists).
*
* @param string $fieldname Field name
* @return string Empty string if not a profile field, or profile field name (without profile_field_)
*/
public static function match_custom_field(string $fieldname): string {
if (preg_match(self::PROFILE_FIELD_REGEX, $fieldname, $matches)) {
return $matches[1];
} else {
return '';
}
}
}

View File

@ -3755,6 +3755,8 @@ function order_in_string($values, $stringformat) {
/**
* Checks if current user is shown any extra fields when listing users.
*
* Does not include any custom profile fields.
*
* @param object $context Context
* @param array $already Array of fields that we're going to show anyway
* so don't bother listing them
@ -3762,46 +3764,8 @@ function order_in_string($values, $stringformat) {
* listed in $already
*/
function get_extra_user_fields($context, $already = array()) {
global $CFG;
// Only users with permission get the extra fields.
if (!has_capability('moodle/site:viewuseridentity', $context)) {
return array();
}
// Split showuseridentity on comma (filter needed in case the showuseridentity is empty).
$extra = array_filter(explode(',', $CFG->showuseridentity));
foreach ($extra as $key => $field) {
if (in_array($field, $already)) {
unset($extra[$key]);
}
}
// If the identity fields are also among hidden fields, make sure the user can see them.
$hiddenfields = array_filter(explode(',', $CFG->hiddenuserfields));
$hiddenidentifiers = array_intersect($extra, $hiddenfields);
if ($hiddenidentifiers) {
if ($context->get_course_context(false)) {
// We are somewhere inside a course.
$canviewhiddenuserfields = has_capability('moodle/course:viewhiddenuserfields', $context);
} else {
// We are not inside a course.
$canviewhiddenuserfields = has_capability('moodle/user:viewhiddendetails', $context);
}
if (!$canviewhiddenuserfields) {
// Remove hidden identifiers from the list.
$extra = array_diff($extra, $hiddenidentifiers);
}
}
// Re-index the entries.
$extra = array_values($extra);
return $extra;
$fields = new \core\user_fields([\core\user_fields::PURPOSE_IDENTITY], [], $already);
return $fields->get_required_fields($context, false);
}
/**
@ -3809,6 +3773,8 @@ function get_extra_user_fields($context, $already = array()) {
* selecting users, returns a string suitable for including in an SQL select
* clause to retrieve those fields.
*
* Does not include any custom profile fields.
*
* @param context $context Context
* @param string $alias Alias of user table, e.g. 'u' (default none)
* @param string $prefix Prefix for field names using AS, e.g. 'u_' (default none)
@ -3816,53 +3782,28 @@ function get_extra_user_fields($context, $already = array()) {
* @return string Partial SQL select clause, beginning with comma, for example ',u.idnumber,u.department' unless it is blank
*/
function get_extra_user_fields_sql($context, $alias='', $prefix='', $already = array()) {
$fields = get_extra_user_fields($context, $already);
$result = '';
// Add punctuation for alias.
if ($alias !== '') {
$alias .= '.';
$fields = new \core\user_fields([\core\user_fields::PURPOSE_IDENTITY], [], $already);
// Note: $joins and $joinparams will always be empty because we turned off profile fields.
[$selects, $joins, $joinparams] = $fields->get_sql($context, false, false, $alias, $prefix);
if ($alias === '') {
// The new code puts {user}. in front of the field names while the old code didn't.
$selects = str_replace('{user}.', '', $selects);
}
foreach ($fields as $field) {
$result .= ', ' . $alias . $field;
if ($prefix) {
$result .= ' AS ' . $prefix . $field;
}
}
return $result;
return $selects;
}
/**
* Returns the display name of a field in the user table. Works for most fields that are commonly displayed to users.
*
* Also works for custom fields.
*
* @param string $field Field name, e.g. 'phone1'
* @return string Text description taken from language file, e.g. 'Phone number'
*/
function get_user_field_name($field) {
// Some fields have language strings which are not the same as field name.
switch ($field) {
case 'url' : {
return get_string('webpage');
}
case 'icq' : {
return get_string('icqnumber');
}
case 'skype' : {
return get_string('skypeid');
}
case 'aim' : {
return get_string('aimid');
}
case 'yahoo' : {
return get_string('yahooid');
}
case 'msn' : {
return get_string('msnid');
}
case 'picture' : {
return get_string('pictureofuser');
}
}
// Otherwise just use the same lang string.
return get_string($field);
return \core\user_fields::get_display_name($field);
}
/**

View File

@ -0,0 +1,70 @@
@core
Feature: Select user identity fields
In order to see who users are at my institution
As an administrator
I can configure which user fields show with lists of users
Background:
Given the following "custom profile fields" exist:
| datatype | shortname | name | param2 |
| text | speciality | Speciality | 255 |
| checkbox | fool | Foolish | |
| text | thesis | Thesis | 100000 |
And the following "users" exist:
| username | department | profile_field_speciality | email |
| user1 | Amphibians | Frogs | email1@example.org |
| user2 | Undead | Zombies | email2@example.org |
And the following "courses" exist:
| shortname | fullname |
| C1 | Course 1 |
And the following "course enrolments" exist:
| user | course | role |
| user1 | C1 | manager |
| user2 | C1 | manager |
Scenario: The admin settings screen should show text custom fields (and let you choose them)
When I log in as "admin"
And I navigate to "Users > Permissions > User policies" in site administration
Then I should see "Speciality" in the "#admin-showuseridentity" "css_element"
And I should not see "Foolish" in the "#admin-showuseridentity" "css_element"
And I should not see "Thesis" in the "#admin-showuseridentity" "css_element"
And I set the field "Speciality" to "1"
And I press "Save changes"
And the field "Speciality" matches value "1"
Scenario: When you choose custom fields, these should be displayed in the 'Browse list of users' screen
Given the following config values are set as admin:
| showuseridentity | username,department,profile_field_speciality |
When I log in as "admin"
And I navigate to "Users > Accounts > Browse list of users" in site administration
Then I should see "Speciality" in the "thead" "css_element"
And I should see "Department" in the "thead" "css_element"
And I should not see "Email" in the "thead" "css_element"
Then I should see "Amphibians" in the "user1" "table_row"
And I should see "Frogs" in the "user1" "table_row"
And I should not see "email1@example.org"
And I should see "Undead" in the "user2" "table_row"
And I should see "Zombies" in the "user2" "table_row"
And I should not see "email2@example.org"
Scenario: When you choose custom fields, these should be displayed in the 'Participants' screen
Given the following config values are set as admin:
| showuseridentity | username,department,profile_field_speciality |
When I am on the "C1" "Course" page logged in as "user1"
And I navigate to course participants
Then I should see "Frogs" in the "user1" "table_row"
And I should see "Zombies" in the "user2" "table_row"
@javascript
Scenario: The user filtering options on the participants screen should work for custom profile fields
Given the following config values are set as admin:
| showuseridentity | username,department,profile_field_speciality |
When I am on the "C1" "Course" page logged in as "admin"
And I navigate to course participants
And I set the field "type" in the "Filter 1" "fieldset" to "Keyword"
And I set the field "Type..." in the "Filter 1" "fieldset" to "Frogs"
# You have to tab out to make it actually apply.
And I press tab
And I click on "Apply filters" "button"
Then I should see "user1" in the "participants" "table"
And I should not see "user2" in the "participants" "table"

View File

@ -0,0 +1,511 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
namespace core;
/**
* Unit tests for \core\user_fields
*
* @package core
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class user_fields_testcase extends \advanced_testcase {
/**
* Tests getting the user picture fields.
*/
public function test_get_picture_fields() {
$this->assertEquals(['id', 'picture', 'firstname', 'lastname', 'firstnamephonetic',
'lastnamephonetic', 'middlename', 'alternatename', 'imagealt', 'email'],
user_fields::get_picture_fields());
}
/**
* Tests getting the user name fields.
*/
public function test_get_name_fields() {
$this->assertEquals(['firstnamephonetic', 'lastnamephonetic', 'middlename', 'alternatename',
'firstname', 'lastname'],
user_fields::get_name_fields());
$this->assertEquals(['firstname', 'lastname',
'firstnamephonetic', 'lastnamephonetic', 'middlename', 'alternatename'],
user_fields::get_name_fields(true));
}
/**
* Tests getting the identity fields.
*/
public function test_get_identity_fields() {
global $DB;
$this->resetAfterTest();
// Create two custom profile fields, one of which is private.
$generator = self::getDataGenerator();
$generator->create_custom_profile_field(['datatype' => 'text', 'shortname' => 'a', 'name' => 'A']);
$generator->create_custom_profile_field(['datatype' => 'text', 'shortname' => 'b', 'name' => 'B',
'visible' => PROFILE_VISIBLE_PRIVATE]);
// Set the extra user fields to include email, department, and both custom profile fields.
set_config('showuseridentity', 'email,department,profile_field_a,profile_field_b');
set_config('hiddenuserfields', 'email');
// Create a test course and a student in the course.
$course = $generator->create_course();
$coursecontext = \context_course::instance($course->id);
$user = $generator->create_user();
$anotheruser = $generator->create_user();
$usercontext = \context_user::instance($anotheruser->id);
$generator->enrol_user($user->id, $course->id, 'student');
// When no context is provided, it does no access checks and should return all specified.
$this->assertEquals(['email', 'department', 'profile_field_a', 'profile_field_b'],
user_fields::get_identity_fields(null));
// If you turn off custom profile fields, you don't get those.
$this->assertEquals(['email', 'department'], user_fields::get_identity_fields(null, false));
// Request in context as an administator.
$this->setAdminUser();
$this->assertEquals(['email', 'department', 'profile_field_a', 'profile_field_b'],
user_fields::get_identity_fields($coursecontext));
$this->assertEquals(['email', 'department'],
user_fields::get_identity_fields($coursecontext, false));
// Request in context as a student - they don't have any of the capabilities to see identity
// fields or profile fields.
$this->setUser($user);
$this->assertEquals([], user_fields::get_identity_fields($coursecontext));
// Give the student the basic identity fields permission.
$roleid = $DB->get_field('role', 'id', ['shortname' => 'student']);
role_change_permission($roleid, $coursecontext, 'moodle/site:viewuseridentity', CAP_ALLOW);
$this->assertEquals(['department', 'profile_field_a'],
user_fields::get_identity_fields($coursecontext));
$this->assertEquals(['department'],
user_fields::get_identity_fields($coursecontext, false));
// Give them permission to view hidden user fields.
role_change_permission($roleid, $coursecontext, 'moodle/course:viewhiddenuserfields', CAP_ALLOW);
$this->assertEquals(['email', 'department', 'profile_field_a'],
user_fields::get_identity_fields($coursecontext));
$this->assertEquals(['email', 'department'],
user_fields::get_identity_fields($coursecontext, false));
// Also give them permission to view all profile fields.
role_change_permission($roleid, $coursecontext, 'moodle/user:viewalldetails', CAP_ALLOW);
$this->assertEquals(['email', 'department', 'profile_field_a', 'profile_field_b'],
user_fields::get_identity_fields($coursecontext));
$this->assertEquals(['email', 'department'],
user_fields::get_identity_fields($coursecontext, false));
// Even if we give them student role in the user context they can't view anything...
$generator->role_assign($roleid, $user->id, $usercontext->id);
$this->assertEquals([], user_fields::get_identity_fields($usercontext));
// Give them basic permission.
role_change_permission($roleid, $usercontext, 'moodle/site:viewuseridentity', CAP_ALLOW);
$this->assertEquals(['department', 'profile_field_a'],
user_fields::get_identity_fields($usercontext));
$this->assertEquals(['department'],
user_fields::get_identity_fields($usercontext, false));
// Give them the hidden user fields permission (it's a different one).
role_change_permission($roleid, $usercontext, 'moodle/user:viewhiddendetails', CAP_ALLOW);
$this->assertEquals(['email', 'department', 'profile_field_a'],
user_fields::get_identity_fields($usercontext));
$this->assertEquals(['email', 'department'],
user_fields::get_identity_fields($usercontext, false));
// Also give them permission to view all profile fields.
role_change_permission($roleid, $usercontext, 'moodle/user:viewalldetails', CAP_ALLOW);
$this->assertEquals(['email', 'department', 'profile_field_a', 'profile_field_b'],
user_fields::get_identity_fields($usercontext));
$this->assertEquals(['email', 'department'],
user_fields::get_identity_fields($usercontext, false));
}
/**
* Tests the get_required_fields function.
*
* This function composes the results of get_identity/name/picture_fields, so we are not going
* to test the details of the identity permissions as that was already covered. Just how they
* are included/combined.
*/
public function test_get_required_fields() {
$this->resetAfterTest();
// Set up some profile fields.
$generator = self::getDataGenerator();
$generator->create_custom_profile_field(['datatype' => 'text', 'shortname' => 'a', 'name' => 'A']);
$generator->create_custom_profile_field(['datatype' => 'text', 'shortname' => 'b', 'name' => 'B']);
set_config('showuseridentity', 'email,department,profile_field_a');
// What happens if you don't ask for anything?
$fields = user_fields::empty();
$this->assertEquals([], $fields->get_required_fields());
// Try each invidual purpose.
$fields = user_fields::for_identity(null);
$this->assertEquals(['email', 'department', 'profile_field_a'], $fields->get_required_fields());
$fields = user_fields::for_userpic();
$this->assertEquals(user_fields::get_picture_fields(), $fields->get_required_fields());
$fields = user_fields::for_name();
$this->assertEquals(user_fields::get_name_fields(), $fields->get_required_fields());
// Try combining them all. There should be no duplicates (e.g. email), and the 'id' field
// should be moved to the start.
$fields = user_fields::for_identity(null)->with_name()->with_userpic();
$this->assertEquals(['id', 'email', 'department', 'profile_field_a', 'picture',
'firstname', 'lastname', 'firstnamephonetic', 'lastnamephonetic', 'middlename',
'alternatename', 'imagealt'], $fields->get_required_fields());
// Add some specified fields to a default result.
$fields = user_fields::for_identity(null, true)->including('city', 'profile_field_b');
$this->assertEquals(['email', 'department', 'profile_field_a', 'city', 'profile_field_b'],
$fields->get_required_fields());
// Remove some fields, one of which actually is in the list.
$fields = user_fields::for_identity(null, true)->excluding('email', 'city');
$this->assertEquals(['department', 'profile_field_a'], $fields->get_required_fields());
// Add and remove fields.
$fields = user_fields::for_identity(null, true)->including('city', 'profile_field_b')->excluding('city', 'department');
$this->assertEquals(['email', 'profile_field_a', 'profile_field_b'],
$fields->get_required_fields());
// Request the list without profile fields, check that still works with both sources.
$fields = user_fields::for_identity(null, false)->including('city', 'profile_field_b')->excluding('city', 'department');
$this->assertEquals(['email'], $fields->get_required_fields());
}
/**
* Tests the get_required_fields function when you use the $limitpurposes parameter.
*/
public function test_get_required_fields_limitpurposes() {
$this->resetAfterTest();
// Set up some profile fields.
$generator = self::getDataGenerator();
$generator->create_custom_profile_field(['datatype' => 'text', 'shortname' => 'a', 'name' => 'A']);
$generator->create_custom_profile_field(['datatype' => 'text', 'shortname' => 'b', 'name' => 'B']);
set_config('showuseridentity', 'email,department,profile_field_a');
// Create a user_fields object with all three purposes, plus included and excluded fields.
$fields = user_fields::for_identity(null, true)->with_name()->with_userpic()
->including('city', 'profile_field_b')->excluding('firstnamephonetic', 'middlename', 'alternatename');
// Check the result with all purposes.
$this->assertEquals(['id', 'email', 'department', 'profile_field_a', 'picture',
'firstname', 'lastname', 'lastnamephonetic', 'imagealt', 'city',
'profile_field_b'],
$fields->get_required_fields([user_fields::PURPOSE_IDENTITY, user_fields::PURPOSE_NAME,
user_fields::PURPOSE_USERPIC, user_fields::CUSTOM_INCLUDE]));
// Limit to identity and custom includes.
$this->assertEquals(['email', 'department', 'profile_field_a', 'city', 'profile_field_b'],
$fields->get_required_fields([user_fields::PURPOSE_IDENTITY, user_fields::CUSTOM_INCLUDE]));
// Limit to name fields.
$this->assertEquals(['firstname', 'lastname', 'lastnamephonetic'],
$fields->get_required_fields([user_fields::PURPOSE_NAME]));
}
/**
* There should be an exception if you try to 'limit' purposes to one that wasn't even included.
*/
public function test_get_required_fields_limitpurposes_not_in_constructor() {
$fields = user_fields::for_identity(null);
$this->expectExceptionMessage('$limitpurposes can only include purposes defined in object');
$fields->get_required_fields([user_fields::PURPOSE_USERPIC]);
}
/**
* Sets up data and a user_fields object for all the get_sql tests.
*
* @return user_fields Constructed user_fields for testing
*/
protected function init_for_sql_tests(): user_fields {
$generator = self::getDataGenerator();
$generator->create_custom_profile_field(['datatype' => 'text', 'shortname' => 'a', 'name' => 'A']);
$generator->create_custom_profile_field(['datatype' => 'text', 'shortname' => 'b', 'name' => 'B']);
// Create a couple of users. One doesn't have a profile field set, so we can test that.
$generator->create_user(['profile_field_a' => 'A1', 'profile_field_b' => 'B1',
'city' => 'C1', 'department' => 'D1', 'email' => 'e1@example.org',
'idnumber' => 'XXX1', 'username' => 'u1']);
$generator->create_user(['profile_field_a' => 'A2',
'city' => 'C2', 'department' => 'D2', 'email' => 'e2@example.org',
'idnumber' => 'XXX2', 'username' => 'u2']);
// It doesn't matter how we construct it (we already tested get_required_fields which is
// where all those values are actually used) so let's just list the fields we want manually.
return user_fields::empty()->including('department', 'city', 'profile_field_a', 'profile_field_b');
}
/**
* Tests getting SQL (and actually using it).
*/
public function test_get_sql_variations() {
global $DB;
$this->resetAfterTest();
$fields = $this->init_for_sql_tests();
user_fields::reset_unique_identifier();
// Basic SQL.
['selects' => $selects, 'joins' => $joins, 'params' => $joinparams, 'mappings' => $mappings] =
(array)$fields->get_sql();
$sql = "SELECT idnumber
$selects
FROM {user}
$joins
WHERE idnumber LIKE ?
ORDER BY idnumber";
$records = $DB->get_records_sql($sql, array_merge($joinparams, ['X%']));
$this->assertCount(2, $records);
$expected1 = (object)['profile_field_a' => 'A1', 'profile_field_b' => 'B1',
'city' => 'C1', 'department' => 'D1', 'idnumber' => 'XXX1'];
$expected2 = (object)['profile_field_a' => 'A2', 'profile_field_b' => null,
'city' => 'C2', 'department' => 'D2', 'idnumber' => 'XXX2'];
$this->assertEquals($expected1, $records['XXX1']);
$this->assertEquals($expected2, $records['XXX2']);
$this->assertEquals([
'department' => '{user}.department',
'city' => '{user}.city',
'profile_field_a' => $DB->sql_compare_text('uf1d_1.data', 255),
'profile_field_b' => $DB->sql_compare_text('uf1d_2.data', 255)], $mappings);
// SQL using named params.
['selects' => $selects, 'joins' => $joins, 'params' => $joinparams] =
(array)$fields->get_sql('', true);
$sql = "SELECT idnumber
$selects
FROM {user}
$joins
WHERE idnumber LIKE :idnum
ORDER BY idnumber";
$records = $DB->get_records_sql($sql, array_merge($joinparams, ['idnum' => 'X%']));
$this->assertCount(2, $records);
$this->assertEquals($expected1, $records['XXX1']);
$this->assertEquals($expected2, $records['XXX2']);
// SQL using alias for user table.
['selects' => $selects, 'joins' => $joins, 'params' => $joinparams, 'mappings' => $mappings] =
(array)$fields->get_sql('u');
$sql = "SELECT idnumber
$selects
FROM {user} u
$joins
WHERE idnumber LIKE ?
ORDER BY idnumber";
$records = $DB->get_records_sql($sql, array_merge($joinparams, ['X%']));
$this->assertCount(2, $records);
$this->assertEquals($expected1, $records['XXX1']);
$this->assertEquals($expected2, $records['XXX2']);
$this->assertEquals([
'department' => 'u.department',
'city' => 'u.city',
'profile_field_a' => $DB->sql_compare_text('uf3d_1.data', 255),
'profile_field_b' => $DB->sql_compare_text('uf3d_2.data', 255)], $mappings);
// Returning prefixed fields.
['selects' => $selects, 'joins' => $joins, 'params' => $joinparams] =
(array)$fields->get_sql('', false, 'u_');
$sql = "SELECT idnumber
$selects
FROM {user}
$joins
WHERE idnumber LIKE ?
ORDER BY idnumber";
$records = $DB->get_records_sql($sql, array_merge($joinparams, ['X%']));
$this->assertCount(2, $records);
$expected1 = (object)['u_profile_field_a' => 'A1', 'u_profile_field_b' => 'B1',
'u_city' => 'C1', 'u_department' => 'D1', 'idnumber' => 'XXX1'];
$this->assertEquals($expected1, $records['XXX1']);
// Renaming the id field. We need to use a different set of fields so it actually has the
// id field.
$fields = user_fields::for_userpic();
['selects' => $selects, 'joins' => $joins, 'params' => $joinparams] =
(array)$fields->get_sql('', false, '', 'userid');
$sql = "SELECT idnumber
$selects
FROM {user}
$joins
WHERE idnumber LIKE ?
ORDER BY idnumber";
$records = $DB->get_records_sql($sql, array_merge($joinparams, ['X%']));
$this->assertCount(2, $records);
// User id was renamed.
$this->assertObjectNotHasAttribute('id', $records['XXX1']);
$this->assertObjectHasAttribute('userid', $records['XXX1']);
// Other fields are normal (just try a couple).
$this->assertObjectHasAttribute('firstname', $records['XXX1']);
$this->assertObjectHasAttribute('imagealt', $records['XXX1']);
// Check the user id is actually right.
$this->assertEquals('XXX1',
$DB->get_field('user', 'idnumber', ['id' => $records['XXX1']->userid]));
// Rename the id field and also use a prefix.
['selects' => $selects, 'joins' => $joins, 'params' => $joinparams] =
(array)$fields->get_sql('', false, 'u_', 'userid');
$sql = "SELECT idnumber
$selects
FROM {user}
$joins
WHERE idnumber LIKE ?
ORDER BY idnumber";
$records = $DB->get_records_sql($sql, array_merge($joinparams, ['X%']));
$this->assertCount(2, $records);
// User id was renamed.
$this->assertObjectNotHasAttribute('id', $records['XXX1']);
$this->assertObjectNotHasAttribute('u_id', $records['XXX1']);
$this->assertObjectHasAttribute('userid', $records['XXX1']);
// Other fields are prefixed (just try a couple).
$this->assertObjectHasAttribute('u_firstname', $records['XXX1']);
$this->assertObjectHasAttribute('u_imagealt', $records['XXX1']);
// Without a leading comma.
['selects' => $selects, 'joins' => $joins, 'params' => $joinparams] =
(array)$fields->get_sql('', false, '', '', false);
$sql = "SELECT $selects
FROM {user}
$joins
WHERE idnumber LIKE ?
ORDER BY idnumber";
$records = $DB->get_records_sql($sql, array_merge($joinparams, ['X%']));
$this->assertCount(2, $records);
foreach ($records as $key => $record) {
// ID should be the first field used by get_records_sql.
$this->assertEquals($key, $record->id);
// Check 2 other sample properties.
$this->assertObjectHasAttribute('firstname', $record);
$this->assertObjectHasAttribute('imagealt', $record);
}
}
/**
* Tests what happens if you use the SQL multiple times in a query (i.e. that it correctly
* creates the different identifiers).
*/
public function test_get_sql_multiple() {
global $DB;
$this->resetAfterTest();
$fields = $this->init_for_sql_tests();
// Inner SQL.
['selects' => $selects1, 'joins' => $joins1, 'params' => $joinparams1] =
(array)$fields->get_sql('u1', true);
// Outer SQL.
$fields2 = user_fields::empty()->including('profile_field_a', 'email');
['selects' => $selects2, 'joins' => $joins2, 'params' => $joinparams2] =
(array)$fields2->get_sql('u2', true);
// Crazy combined query.
$sql = "SELECT username, details.profile_field_b AS innerb, details.city AS innerc
$selects2
FROM {user} u2
$joins2
LEFT JOIN (
SELECT u1.id
$selects1
FROM {user} u1
$joins1
WHERE idnumber LIKE :idnum
) details ON details.id = u2.id
ORDER BY username";
$records = $DB->get_records_sql($sql, array_merge($joinparams1, $joinparams2, ['idnum' => 'X%']));
// The left join won't match for admin.
$this->assertNull($records['admin']->innerb);
$this->assertNull($records['admin']->innerc);
// It should match for one of the test users though.
$expected1 = (object)['username' => 'u1', 'innerb' => 'B1', 'innerc' => 'C1',
'profile_field_a' => 'A1', 'email' => 'e1@example.org'];
$this->assertEquals($expected1, $records['u1']);
}
/**
* Tests the get_sql function when there are no fields to retrieve.
*/
public function test_get_sql_nothing() {
$fields = user_fields::empty();
['selects' => $selects, 'joins' => $joins, 'params' => $joinparams] = (array)$fields->get_sql();
$this->assertEquals('', $selects);
$this->assertEquals('', $joins);
$this->assertEquals([], $joinparams);
}
/**
* Tests get_sql when there are no custom fields; in this scenario, the joins and joinparams
* are always blank.
*/
public function test_get_sql_no_custom_fields() {
$fields = user_fields::empty()->including('city', 'country');
['selects' => $selects, 'joins' => $joins, 'params' => $joinparams, 'mappings' => $mappings] =
(array)$fields->get_sql('u');
$this->assertEquals(', u.city, u.country', $selects);
$this->assertEquals('', $joins);
$this->assertEquals([], $joinparams);
$this->assertEquals(['city' => 'u.city', 'country' => 'u.country'], $mappings);
}
/**
* Tests the format of the $selects string, which is important particularly for backward
* compatibility.
*/
public function test_get_sql_selects_format() {
global $DB;
$this->resetAfterTest();
user_fields::reset_unique_identifier();
$generator = self::getDataGenerator();
$generator->create_custom_profile_field(['datatype' => 'text', 'shortname' => 'a', 'name' => 'A']);
// When we list fields that include custom profile fields...
$fields = user_fields::empty()->including('id', 'profile_field_a');
// Supplying an alias: all fields have alias.
$selects = $fields->get_sql('u')->selects;
$this->assertEquals(', u.id, ' . $DB->sql_compare_text('uf1d_1.data', 255) . ' AS profile_field_a', $selects);
// No alias: all files have {user} because of the joins.
$selects = $fields->get_sql()->selects;
$this->assertEquals(', {user}.id, ' . $DB->sql_compare_text('uf2d_1.data', 255) . ' AS profile_field_a', $selects);
// When the list doesn't include custom profile fields...
$fields = user_fields::empty()->including('id', 'city');
// Supplying an alias: all fields have alias.
$selects = $fields->get_sql('u')->selects;
$this->assertEquals(', u.id, u.city', $selects);
// No alias: fields do not have alias at all.
$selects = $fields->get_sql()->selects;
$this->assertEquals(', id, city', $selects);
}
}

View File

@ -35,6 +35,8 @@ information provided here is intended especially for developers.
See https://docs.moodle.org/dev/Modal_and_AJAX_forms for more details.
* Admin setting admin_setting_configmulticheckbox now supports lazy-loading the options list by
supplying a callback function instead of an array of options.
* A new core API class \core\user_fields provides ways to get lists of user fields, and SQL related to
those fields.
=== 3.10 ===
* PHPUnit has been upgraded to 8.5. That comes with a few changes:

View File

@ -848,6 +848,36 @@ function profile_save_custom_fields($userid, $profilefields) {
}
}
/**
* Gets basic data about custom profile fields. This is minimal data that is cached within the
* current request for all fields so that it can be used quickly.
*
* @param string $shortname Shortname of custom profile field
* @return array Array with id, name, and visible fields
*/
function profile_get_custom_field_data_by_shortname(string $shortname): array {
global $DB;
$cache = \cache::make_from_params(cache_store::MODE_REQUEST, 'core_profile', 'customfields',
[], ['simplekeys' => true, 'simpledata' => true]);
$data = $cache->get($shortname);
if (!$data) {
// If we don't have data, we get and cache it for all fields to avoid multiple DB requests.
$fields = $DB->get_records('user_info_field', null, '', 'id, shortname, name, visible');
foreach ($fields as $field) {
$cache->set($field->shortname, (array)$field);
if ($field->shortname === $shortname) {
$data = (array)$field;
}
}
if (!$data) {
throw new \coding_exception('Unknown custom field: ' . $shortname);
}
}
return $data;
}
/**
* Trigger a user profile viewed event.
*

View File

@ -240,4 +240,48 @@ class core_user_profilelib_testcase extends advanced_testcase {
$this->assertObjectHasAttribute('house', $profilefields2);
$this->assertNull($profilefields2->house);
}
/**
* Tests the profile_get_custom_field_data_by_shortname function when working normally.
*/
public function test_profile_get_custom_field_data_by_shortname_normal() {
global $DB, $CFG;
require_once($CFG->dirroot . '/user/profile/lib.php');
$this->resetAfterTest();
// Create 3 profile fields.
$generator = $this->getDataGenerator();
$field1 = $generator->create_custom_profile_field(['datatype' => 'text',
'shortname' => 'speciality', 'name' => 'Speciality',
'visible' => PROFILE_VISIBLE_ALL]);
$field2 = $generator->create_custom_profile_field(['datatype' => 'menu',
'shortname' => 'veggie', 'name' => 'Vegetarian',
'visible' => PROFILE_VISIBLE_PRIVATE]);
// Get the first field data and check it is correct.
$data = profile_get_custom_field_data_by_shortname('speciality');
$this->assertEquals('Speciality', $data['name']);
$this->assertEquals(PROFILE_VISIBLE_ALL, $data['visible']);
$this->assertEquals($field1->id, $data['id']);
// Get the second field data, checking there is no database query this time.
$before = $DB->perf_get_queries();
$data = profile_get_custom_field_data_by_shortname('veggie');
$this->assertEquals($before, $DB->perf_get_queries());
$this->assertEquals('Vegetarian', $data['name']);
$this->assertEquals(PROFILE_VISIBLE_PRIVATE, $data['visible']);
$this->assertEquals($field2->id, $data['id']);
}
/**
* Tests the profile_get_custom_field_data_by_shortname function with a field that doesn't exist.
*/
public function test_profile_get_custom_field_data_by_shortname_missing() {
global $CFG;
require_once($CFG->dirroot . '/user/profile/lib.php');
$this->expectExceptionMessage('Unknown custom field: speciality');
profile_get_custom_field_data_by_shortname('speciality');
}
}