This commit is contained in:
Ilya Tregubov 2023-09-15 10:24:08 +08:00
commit 4355e38e88
No known key found for this signature in database
GPG Key ID: 0F58186F748E55C1
19 changed files with 225 additions and 53 deletions

View File

@ -708,7 +708,7 @@ class external extends external_api {
$fields .= ',' . implode(',', $extrafields);
}
list($sql, $params) = users_search_sql($query, '', false, $extrafields, $excludedusers);
list($sql, $params) = users_search_sql($query, '', USER_SEARCH_STARTS_WITH, $extrafields, $excludedusers);
$users = $DB->get_records_select('user', $sql, $params, $sort, $fields, 0, 30);
$useroptions = [];
foreach ($users as $user) {

View File

@ -874,7 +874,7 @@ class external extends external_api {
$fields = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
$extrasearchfields = $userfieldsapi->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]);
list($wheresql, $whereparams) = users_search_sql($query, 'u', true, $extrasearchfields);
list($wheresql, $whereparams) = users_search_sql($query, 'u', USER_SEARCH_CONTAINS, $extrasearchfields);
list($sortsql, $sortparams) = users_order_by_sql('u', $query, $context);
$countsql = "SELECT COUNT('x') FROM {user} u WHERE $wheresql AND u.id $filtercapsql";

View File

@ -58,7 +58,7 @@ $outcome->success = true;
$outcome->response = new stdClass();
$outcome->error = '';
$searchanywhere = get_user_preferences('userselector_searchanywhere', false);
$searchanywhere = get_user_preferences('userselector_searchtype') === USER_SEARCH_CONTAINS;
switch ($action) {
case 'unenrol':

View File

@ -274,7 +274,7 @@ class course_enrolment_manager {
// Search condition.
// TODO Does not support custom user profile fields (MDL-70456).
$extrafields = fields::get_identity_fields($this->get_context(), false);
list($sql, $params) = users_search_sql($this->searchfilter, 'u', true, $extrafields);
list($sql, $params) = users_search_sql($this->searchfilter, 'u', USER_SEARCH_CONTAINS, $extrafields);
// Role condition.
if ($this->rolefilter) {

View File

@ -60,7 +60,7 @@ $outcome->response = new stdClass();
$outcome->error = '';
$outcome->count = 0;
$searchanywhere = get_user_preferences('userselector_searchanywhere', false);
$searchanywhere = get_user_preferences('userselector_searchtype') === USER_SEARCH_CONTAINS;
switch ($action) {
case 'enrol':

View File

@ -138,7 +138,7 @@ class helper {
}
$params = array();
if (!empty($search)) {
list($filtersql, $params) = users_search_sql($search, 'u', true, $extrafields);
list($filtersql, $params) = users_search_sql($search, 'u', USER_SEARCH_CONTAINS, $extrafields);
$filtersql .= ' AND ';
} else {
$filtersql = '';

View File

@ -2369,7 +2369,10 @@ $string['userpic'] = 'User picture';
$string['users'] = 'Users';
$string['userselectorautoselectunique'] = 'If only one user matches the search, select them automatically';
$string['userselectorpreserveselected'] = 'Keep selected users, even if they no longer match the search';
$string['userselectorsearchanywhere'] = 'Match the search text anywhere in the displayed fields';
$string['userselectorsearchmatching'] = 'Matching:';
$string['userselectorsearchfromstart'] = 'from start';
$string['userselectorsearchanywhere'] = 'anywhere';
$string['userselectorsearchexactmatchonly'] = 'exact matches only';
$string['usersnew'] = 'New users';
$string['usersnoaccesssince'] = 'Inactive for more than';
$string['userpreferences'] = 'User preferences';

View File

@ -243,7 +243,7 @@ class core_user {
}
// Start building the WHERE clause based on name.
list ($where, $whereparams) = users_search_sql($query, 'u', false);
list ($where, $whereparams) = users_search_sql($query, 'u');
// We allow users to search with extra identity fields (as well as name) but only if they
// have the permission to display those identity fields.
@ -1021,10 +1021,10 @@ class core_user {
'default' => false,
'permissioncallback' => [static::class, 'is_current_user'],
];
$preferences['userselector_searchanywhere'] = [
'type' => PARAM_BOOL,
$preferences['userselector_searchtype'] = [
'type' => PARAM_INT,
'null' => NULL_NOT_ALLOWED,
'default' => false,
'default' => USER_SEARCH_STARTS_WITH,
'permissioncallback' => [static::class, 'is_current_user'],
];
$preferences['question_bank_advanced_search'] = [

View File

@ -52,6 +52,12 @@ define('MAX_COURSE_CATEGORIES', 10000);
if (!defined('LASTACCESS_UPDATE_SECS')) {
define('LASTACCESS_UPDATE_SECS', 60);
}
/**
* The constant value when we use the search option.
*/
define('USER_SEARCH_STARTS_WITH', 0);
define('USER_SEARCH_CONTAINS', 1);
define('USER_SEARCH_EXACT_MATCH', 2);
/**
* Returns $user object of the main admin user
@ -216,8 +222,8 @@ function search_users($courseid, $groupid, $searchtext, $sort='', array $excepti
* @param string $search the text to search for (empty string = find all)
* @param string $u the table alias for the user table in the query being
* built. May be ''.
* @param bool $searchanywhere If true (default), searches in the middle of
* names, otherwise only searches at start
* @param int $searchtype If 0(default): searches at start, 1: searches in the middle of names
* 2: search exact match.
* @param array $extrafields Array of extra user fields to include in search, must be prefixed with table alias if they are not in
* the user table.
* @param array $exclude Array of user ids to exclude (empty = don't exclude)
@ -227,7 +233,7 @@ function search_users($courseid, $groupid, $searchtext, $sort='', array $excepti
* where clause the query, and an associative array containing any required
* parameters (using named placeholders).
*/
function users_search_sql(string $search, string $u = 'u', bool $searchanywhere = true, array $extrafields = [],
function users_search_sql(string $search, string $u = 'u', int $searchtype = USER_SEARCH_STARTS_WITH, array $extrafields = [],
array $exclude = null, array $includeonly = null): array {
global $DB, $CFG;
$params = array();
@ -237,7 +243,6 @@ function users_search_sql(string $search, string $u = 'u', bool $searchanywhere
$u .= '.';
}
// If we have a $search string, put a field LIKE '$search%' condition on each field.
if ($search) {
$conditions = array(
$DB->sql_fullname($u . 'firstname', $u . 'lastname'),
@ -247,14 +252,26 @@ function users_search_sql(string $search, string $u = 'u', bool $searchanywhere
// Add the table alias for the user table if the field doesn't already have an alias.
$conditions[] = strpos($field, '.') !== false ? $field : $u . $field;
}
if ($searchanywhere) {
$searchparam = '%' . $search . '%';
} else {
$searchparam = $search . '%';
switch ($searchtype) {
case USER_SEARCH_STARTS_WITH:
// Put a field LIKE 'search%' condition on each field.
$searchparam = $search . '%';
break;
case USER_SEARCH_CONTAINS:
// Put a field LIKE '$search%' condition on each field.
$searchparam = '%' . $search . '%';
break;
case USER_SEARCH_EXACT_MATCH:
// Match exact the $search string.
$searchparam = $search;
break;
}
$i = 0;
foreach ($conditions as $key => $condition) {
$conditions[$key] = $DB->sql_like($condition, ":con{$i}00", false, false);
if ($searchtype === USER_SEARCH_EXACT_MATCH) {
$conditions[$key] = "$condition = :con{$i}00";
}
$params["con{$i}00"] = $searchparam;
$i++;
}

View File

@ -3594,5 +3594,12 @@ privatefiles,moodle|/user/files.php';
upgrade_main_savepoint(true, 2023090200.01);
}
if ($oldversion < 2023091300.03) {
// Delete all the searchanywhere prefs in user_preferences table.
$DB->delete_records('user_preferences', ['name' => 'userselector_searchanywhere']);
// Main savepoint reached.
upgrade_main_savepoint(true, 2023091300.03);
}
return true;
}

View File

@ -77,42 +77,48 @@ class datalib_test extends \advanced_testcase {
$user2 = self::getDataGenerator()->create_user($user2);
// Search by name (anywhere in text).
list($sql, $params) = users_search_sql('User Test 2', '');
list($sql, $params) = users_search_sql('User Test 2', '', USER_SEARCH_CONTAINS);
$results = $DB->get_records_sql("SELECT id FROM {user} WHERE $sql ORDER BY username", $params);
$this->assertFalse(array_key_exists($user1->id, $results));
$this->assertTrue(array_key_exists($user2->id, $results));
// Search by (most of) full name.
list($sql, $params) = users_search_sql('First Name User Test 2 Last Name User', '');
list($sql, $params) = users_search_sql('First Name User Test 2 Last Name User', '', USER_SEARCH_CONTAINS);
$results = $DB->get_records_sql("SELECT id FROM {user} WHERE $sql ORDER BY username", $params);
$this->assertFalse(array_key_exists($user1->id, $results));
$this->assertTrue(array_key_exists($user2->id, $results));
// Search by name (start of text) valid or not.
list($sql, $params) = users_search_sql('User Test 2', '', false);
list($sql, $params) = users_search_sql('User Test 2', '');
$results = $DB->get_records_sql("SELECT id FROM {user} WHERE $sql ORDER BY username", $params);
$this->assertEquals(0, count($results));
list($sql, $params) = users_search_sql('First Name User Test 2', '', false);
list($sql, $params) = users_search_sql('First Name User Test 2', '');
$results = $DB->get_records_sql("SELECT id FROM {user} WHERE $sql ORDER BY username", $params);
$this->assertFalse(array_key_exists($user1->id, $results));
$this->assertTrue(array_key_exists($user2->id, $results));
// Search by extra fields included or not (address).
list($sql, $params) = users_search_sql('Test Street', '', true);
list($sql, $params) = users_search_sql('Test Street', '', USER_SEARCH_CONTAINS);
$results = $DB->get_records_sql("SELECT id FROM {user} WHERE $sql ORDER BY username", $params);
$this->assertCount(0, $results);
list($sql, $params) = users_search_sql('Test Street', '', true, array('address'));
list($sql, $params) = users_search_sql('Test Street', '', USER_SEARCH_CONTAINS, array('address'));
$results = $DB->get_records_sql("SELECT id FROM {user} WHERE $sql ORDER BY username", $params);
$this->assertCount(2, $results);
// Exclude user.
list($sql, $params) = users_search_sql('User Test', '', true, array(), array($user1->id));
list($sql, $params) = users_search_sql('User Test', '', USER_SEARCH_CONTAINS, array(), array($user1->id));
$results = $DB->get_records_sql("SELECT id FROM {user} WHERE $sql ORDER BY username", $params);
$this->assertFalse(array_key_exists($user1->id, $results));
$this->assertTrue(array_key_exists($user2->id, $results));
// Include only user.
list($sql, $params) = users_search_sql('User Test', '', true, array(), array(), array($user1->id));
list($sql, $params) = users_search_sql('User Test', '', USER_SEARCH_CONTAINS, array(), array(), array($user1->id));
$results = $DB->get_records_sql("SELECT id FROM {user} WHERE $sql ORDER BY username", $params);
$this->assertTrue(array_key_exists($user1->id, $results));
$this->assertFalse(array_key_exists($user2->id, $results));
// Exact match only.
[$sql, $params] = users_search_sql('Last Name User Test 1', '', USER_SEARCH_EXACT_MATCH, [], null, null, true);
$results = $DB->get_records_sql("SELECT id FROM {user} WHERE $sql ORDER BY username", $params);
$this->assertTrue(array_key_exists($user1->id, $results));
$this->assertFalse(array_key_exists($user2->id, $results));
@ -120,7 +126,7 @@ class datalib_test extends \advanced_testcase {
// Join with another table and use different prefix.
set_user_preference('amphibian', 'frog', $user1);
set_user_preference('amphibian', 'salamander', $user2);
list($sql, $params) = users_search_sql('User Test 1', 'qq');
list($sql, $params) = users_search_sql('User Test 1', 'qq', USER_SEARCH_CONTAINS);
$results = $DB->get_records_sql("
SELECT up.id, up.value
FROM {user} qq
@ -135,7 +141,7 @@ class datalib_test extends \advanced_testcase {
// Join with another table and include other table fields in search.
set_user_preference('reptile', 'snake', $user1);
set_user_preference('reptile', 'lizard', $user2);
list($sql, $params) = users_search_sql('snake', 'qq', true, ['up.value']);
list($sql, $params) = users_search_sql('snake', 'qq', USER_SEARCH_CONTAINS, ['up.value']);
$results = $DB->get_records_sql("
SELECT up.id, up.value
FROM {user} qq

View File

@ -182,6 +182,9 @@ being forced open in all behat tests.
* The $CFG->svgicons setting has been removed because all modern browsers now handle SVG files correctly.
* The method `\core_renderer->supportemail()` has an updated signature. It now allows a second optional parameter.
Set it to true if you want to embed the generated link in other inline content.
* The users_search_sql function parameter $searchanywhere has been change to $searchtype for different type of search. $searchtype is a int parameter and has three constant value:
USER_SEARCH_STARTS_WITH: 0, USER_SEARCH_CONTAINS: 1, USER_SEARCH_EXACT_MATCH: 2
See MDL-78312 for further information.
=== 4.2 ===

View File

@ -68,7 +68,7 @@ class search_identity extends external_api {
$fields = \core_user\fields::for_name()->with_identity($context, false);
$extrafields = $fields->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]);
list($searchsql, $searchparams) = users_search_sql($query, '', true, $extrafields);
list($searchsql, $searchparams) = users_search_sql($query, '', USER_SEARCH_CONTAINS, $extrafields);
list($sortsql, $sortparams) = users_order_by_sql('', $query, $context);
$params = array_merge($searchparams, $sortparams);

View File

@ -58,9 +58,9 @@ abstract class user_selector_base {
protected $preserveselected = false;
/** @var boolean If only one user matches the search, should we select them automatically. */
protected $autoselectunique = false;
/** @var boolean When searching, do we only match the starts of fields (better performance)
* or do we match occurrences anywhere? */
protected $searchanywhere = false;
/** @var int When searching, do we only match the starts of fields (better performance)
* or do we match occurrences anywhere or do we match exact the fields. */
protected $searchtype = USER_SEARCH_STARTS_WITH;
/** @var mixed This is used by get selected users */
protected $validatinguserids = null;
@ -156,8 +156,7 @@ abstract class user_selector_base {
// Read the user prefs / optional_params that we use.
$this->preserveselected = $this->initialise_option('userselector_preserveselected', $this->preserveselected);
$this->autoselectunique = $this->initialise_option('userselector_autoselectunique', $this->autoselectunique);
$this->searchanywhere = $this->initialise_option('userselector_searchanywhere', $this->searchanywhere);
$this->searchtype = (int) $this->initialise_option('userselector_searchtype', $this->searchtype, PARAM_INT);
if (!empty($CFG->maxusersperpage)) {
$this->maxusersperpage = $CFG->maxusersperpage;
}
@ -278,7 +277,6 @@ abstract class user_selector_base {
$this->name . '_clearbutton" value="' . get_string('clear') . '" class="btn btn-secondary"/>';
// And the search options.
$optionsoutput = false;
if (!user_selector_base::$searchoptionsoutput) {
$output .= print_collapsible_region_start('', 'userselector_options',
get_string('searchoptions'), 'userselector_optionscollapsed', true, true);
@ -286,8 +284,7 @@ abstract class user_selector_base {
get_string('userselectorpreserveselected'));
$output .= $this->option_checkbox('autoselectunique', $this->autoselectunique,
get_string('userselectorautoselectunique'));
$output .= $this->option_checkbox('searchanywhere', $this->searchanywhere,
get_string('userselectorsearchanywhere'));
$output .= $this->output_searchtype_radios();
$output .= print_collapsible_region_end(true);
$PAGE->requires->js_init_call('M.core_user.init_user_selector_options_tracker', array(), false, self::$jsmodule);
@ -499,8 +496,7 @@ abstract class user_selector_base {
$extrafields = $this->includecustomfields
? array_values($this->userfieldsmappings)
: $this->extrafields;
return users_search_sql($search, $u, $this->searchanywhere, $extrafields,
return users_search_sql($search, $u, $this->searchtype, $extrafields,
$this->exclude, $this->validatinguserids);
}
@ -644,10 +640,11 @@ abstract class user_selector_base {
*
* @param string $name
* @param mixed $default
* @param string $paramtype allow the option to custom param type. default is bool
* @return mixed|null|string
*/
private function initialise_option($name, $default) {
$param = optional_param($name, null, PARAM_BOOL);
private function initialise_option($name, $default, $paramtype = PARAM_BOOL) {
$param = optional_param($name, null, $paramtype);
if (is_null($param)) {
return get_user_preferences($name, $default);
} else {
@ -673,7 +670,7 @@ abstract class user_selector_base {
$name = 'userselector_' . $name;
// For the benefit of brain-dead IE, the id must be different from the name of the hidden form field above.
// It seems that document.getElementById('frog') in IE will return and element with name="frog".
$output = '<div class="form-check"><input type="hidden" name="' . $name . '" value="0" />' .
$output = '<div class="form-check justify-content-start ml-1"><input type="hidden" name="' . $name . '" value="0" />' .
'<label class="form-check-label" for="' . $name . 'id">' .
'<input class="form-check-input" type="checkbox" id="' . $name . 'id" name="' . $name .
'" value="1"' . $checked . ' /> ' . $label .
@ -682,6 +679,48 @@ abstract class user_selector_base {
return $output;
}
/**
* Get all the data for each input in the user selector search type.
*
* @param string $name
* @param bool $checked
* @param string $label
* @param int $value
* @param string $class
* @return array a list of attributes for input.
*/
private function get_radio_searchtype_data(string $name, bool $checked, string $label, int $value, string $class): array {
$name = 'userselector_' . $name;
$id = $name . 'id';
$attrs = [
'value' => $value,
'id' => $id,
'name' => $name,
'class' => $class,
'label' => $label,
];
if ($checked) {
$attrs['checked'] = true;
}
return $attrs;
}
/**
* Output the search type radio buttons.
*
* @return string
*/
private function output_searchtype_radios(): string {
global $OUTPUT;
$fields[] = $this->get_radio_searchtype_data('searchexactmatchesonly', $this->searchtype === USER_SEARCH_EXACT_MATCH,
get_string('userselectorsearchexactmatchonly'), USER_SEARCH_EXACT_MATCH, 'mr-1');
$fields[] = $this->get_radio_searchtype_data('searchfromstart', $this->searchtype === USER_SEARCH_STARTS_WITH,
get_string('userselectorsearchfromstart'), USER_SEARCH_STARTS_WITH, 'mr-1');
$fields[] = $this->get_radio_searchtype_data('searchanywhere', $this->searchtype === USER_SEARCH_CONTAINS,
get_string('userselectorsearchanywhere'), USER_SEARCH_CONTAINS, 'mr-1');
return $OUTPUT->render_from_template('core_user/form_user_selector_searchtype', (object) ['fields' => $fields]);
}
/**
* Initialises JS for this control.
*
@ -689,7 +728,7 @@ abstract class user_selector_base {
* @return string any HTML needed here.
*/
protected function initialise_javascript($search) {
global $USER, $PAGE, $OUTPUT;
global $USER, $PAGE;
$output = '';
// Put the options into the session, to allow search.php to respond to the ajax requests.
@ -700,7 +739,7 @@ abstract class user_selector_base {
// Initialise the selector.
$PAGE->requires->js_init_call(
'M.core_user.init_user_selector',
array($this->name, $hash, $this->extrafields, $search),
array($this->name, $hash, $this->extrafields, $search, $this->searchtype),
false,
self::$jsmodule
);

View File

@ -25,8 +25,9 @@ M.core_user.get_user_selector = function (name) {
* @param {string} hash the hash that identifies this selector in the user's session.
* @param {array} extrafields extra fields we are displaying for each user in addition to fullname.
* @param {string} lastsearch The last search that took place
* @param {int} searchtype the last search option that took place
*/
M.core_user.init_user_selector = function (Y, name, hash, extrafields, lastsearch) {
M.core_user.init_user_selector = function(Y, name, hash, extrafields, lastsearch, searchtype) {
// Creates a new user_selector object
var user_selector = {
/** This id/name used for this control in the HTML. */
@ -50,6 +51,8 @@ M.core_user.init_user_selector = function (Y, name, hash, extrafields, lastsearc
/** Whether any options where selected last time we checked. Used by
* handle_selection_change to track when this status changes. */
selectionempty : true,
/** The last search option that we use for*/
searchtype: searchtype,
/**
* Initialises the user selector object
* @constructor
@ -69,7 +72,9 @@ M.core_user.init_user_selector = function (Y, name, hash, extrafields, lastsearc
this.listbox.on('change', this.handle_selection_change, this);
// And when the search any substring preference changes. Do an immediate re-search.
Y.one('#userselector_searchanywhereid').on('click', this.handle_searchanywhere_change, this);
Y.one('#userselector_searchfromstartid').on('click', this.handle_searchtype_change, this);
Y.one('#userselector_searchanywhereid').on('click', this.handle_searchtype_change, this);
Y.one('#userselector_searchexactmatchesonlyid').on('click', this.handle_searchtype_change, this);
// Define our custom event.
//this.createEvent('selectionchanged');
@ -114,13 +119,22 @@ M.core_user.init_user_selector = function (Y, name, hash, extrafields, lastsearc
this.selectionempty = isselectionempty;
},
/**
* Trigger a re-search when the 'search any substring' option is changed.
* Trigger a re-search and set the user prefs when the search radio option is changed.
*
* @param {Y.Event} e the change event.
*/
handle_searchanywhere_change : function() {
handle_searchtype_change: function(e) {
this.clear_search_radio_state();
e.currentTarget.set('checked', 1);
this.searchtype = e.currentTarget.get('value');
require(['core_user/repository'], function(UserRepository) {
UserRepository.setUserPreference('userselector_searchtype', e.currentTarget.get('value'));
});
if (this.lastsearch != '' && this.get_search_text() != '') {
this.send_query(true);
}
},
/**
* Click handler for the clear button..
*/
@ -129,6 +143,16 @@ M.core_user.init_user_selector = function (Y, name, hash, extrafields, lastsearc
this.clearbutton.set('disabled',true);
this.send_query(false);
},
/**
* Clear all checked state in the radio search option.
*/
clear_search_radio_state: function() {
Y.one('#userselector_searchfromstartid').set('checked', 0);
Y.one('#userselector_searchanywhereid').set('checked', 0);
Y.one('#userselector_searchexactmatchesonlyid').set('checked', 0);
},
/**
* Fires off the ajax search request.
*/
@ -146,10 +170,10 @@ M.core_user.init_user_selector = function (Y, name, hash, extrafields, lastsearc
Y.Object.each(this.iotransactions, function(trans) {
trans.abort();
});
var iotrans = Y.io(M.cfg.wwwroot + '/user/selector/search.php', {
method: 'POST',
data: 'selectorid=' + hash + '&sesskey=' + M.cfg.sesskey + '&search=' + value + '&userselector_searchanywhere=' + this.get_option('searchanywhere'),
data: 'selectorid=' + hash + '&sesskey=' + M.cfg.sesskey +
'&search=' + value + '&userselector_searchtype=' + this.searchtype,
on: {
complete: this.handle_response
},
@ -355,7 +379,6 @@ M.core_user.init_user_selector_options_tracker = function(Y) {
var settings = [
'userselector_preserveselected',
'userselector_autoselectunique',
'userselector_searchanywhere'
];
for (var s in settings) {
var setting = settings[s];

View File

@ -0,0 +1,47 @@
{{!
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/>.
}}
{{!
@template core_user/form_user_selector_searchtype
Template for the form_user_selector search type.
Context variables required for this template:
* defaultvalue int - The user's full name.
* fields list - the input element attributes and label
Example context (json):
{
"fields": [
{
"id": "id",
"name": "userselector_searchexactmatchesonly",
"class": "class",
"label": "Search anywhere",
"checked": true
}
]
}
}}
<div class="form-check justify-content-start ml-1">
<span class="mr-1">{{#str}}userselectorsearchmatching, core{{/str}}</span>
{{#fields}}
<label for="{{id}}">
<input id="{{id}}" type="radio" value="{{value}}" name="{{name}}" class="{{class}}" {{#checked}}checked{{/checked}}>
{{label}}
</label>
{{/fields}}
</div>

View File

@ -0,0 +1,24 @@
@core @core_user @javascript
Feature: The admin can check student permission in moodle system.
In order to check permission of a user in moodle
As an admin user
I can search student and see their permission
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| user1 | User | 1 | user1@example.com |
Scenario: The search setting is saved for each user.
Given I log in as "admin"
And I navigate to "Users > Permissions > Check system permissions" in site administration
And I follow "Search options"
And the field "from start" matches value "1"
And I click on "anywhere" "radio"
And I click on "Keep selected users, even if they no longer match the search" "checkbox"
And I click on "If only one user matches the search, select them automatically" "checkbox"
And I reload the page
Then the field "from start" matches value "0"
And the field "anywhere" matches value "1"
And the field "Keep selected users, even if they no longer match the search" matches value "1"
And the field "If only one user matches the search, select them automatically" matches value "1"

View File

@ -22,6 +22,9 @@ This files describes API changes for code that uses the user API.
- `user_get_participants`
- `user_get_participants_sql`
- `user_get_total_participants`
* The users_search_sql function parameter $searchanywhere has been change to $searchtype for different type of search. $searchtype is a int parameter and has three constant value:
USER_SEARCH_STARTS_WITH: 0, USER_SEARCH_CONTAINS: 1, USER_SEARCH_EXACT_MATCH: 2
users_search_sql('User Test 2', '', false) => users_search_sql('Test Street', '', USER_SEARCH_CONTAINS)
=== 4.2 ===

View File

@ -29,7 +29,7 @@
defined('MOODLE_INTERNAL') || die();
$version = 2023091300.02; // YYYYMMDD = weekly release date of this DEV branch.
$version = 2023091300.03; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.
$release = '4.3dev+ (Build: 20230913)'; // Human-friendly version name