MDL-52198 cbe: Add a user selector

For now this is only in tool_lp - for core we would wrap this with an mform element.
This commit is contained in:
Frederic Massart
2016-04-18 10:58:44 +08:00
parent a495d1a18b
commit 80583e0dff
6 changed files with 504 additions and 1 deletions
+1
View File
@@ -0,0 +1 @@
define(["jquery","core/ajax","core/templates"],function(a,b,c){return{processResults:function(b,c){var d=[];return a.each(c,function(a,b){d.push({value:b.id,label:b._label})}),d},transport:function(d,e,f,g){var h,i=a(d).data("capability");"undefined"==typeof i&&(i=""),h=b.call([{methodname:"tool_lp_search_users",args:{query:e,capability:i}}]),h[0].then(function(b){var d=[],e=0;return a.each(b.users,function(b,e){var f=e,g=[];a.each(["idnumber","email","phone1","phone2","department","institution"],function(a,b){"undefined"!=typeof e[b]&&(f.hasidentity=!0,g.push(e[b]))}),f.identity=g.join(", "),d.push(c.render("tool_lp/form-user-selector-suggestion",f))}),a.when.apply(a.when,d).then(function(){var c=arguments;a.each(b.users,function(a,b){b._label=c[e],e++}),f(b.users)})},g)}}});
@@ -0,0 +1,89 @@
// 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/>.
/**
* User selector module.
*
* @module tool_lp/form-user-selector
* @class form-user-selector
* @package tool_lp
* @copyright 2015 Frédéric Massart - FMCorz.net
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(['jquery', 'core/ajax', 'core/templates'], function($, Ajax, Templates) {
return /** @alias module:tool_lp/form-user-selector */ {
processResults: function(selector, results) {
var users = [];
$.each(results, function(index, user) {
users.push({
value: user.id,
label: user._label
});
});
return users;
},
transport: function(selector, query, success, failure) {
var promise;
var capability = $(selector).data('capability');
if (typeof capability === "undefined") {
capability = '';
}
promise = Ajax.call([{
methodname: 'tool_lp_search_users',
args: {
query: query,
capability: capability
}
}]);
promise[0].then(function(results) {
var promises = [],
i = 0;
// Render the label.
$.each(results.users, function(index, user) {
var ctx = user,
identity = [];
$.each(['idnumber', 'email', 'phone1', 'phone2', 'department', 'institution'], function(i, k) {
if (typeof user[k] !== 'undefined') {
ctx.hasidentity = true;
identity.push(user[k]);
}
});
ctx.identity = identity.join(', ');
promises.push(Templates.render('tool_lp/form-user-selector-suggestion', ctx));
});
// Apply the label to the results.
return $.when.apply($.when, promises).then(function() {
var args = arguments;
$.each(results.users, function(index, user) {
user._label = args[i];
i++;
});
success(results.users);
});
}, failure);
}
};
});
+238
View File
@@ -30,6 +30,7 @@ use context;
use context_system;
use context_course;
use context_user;
use coding_exception;
use external_api;
use external_function_parameters;
use external_value;
@@ -3298,4 +3299,241 @@ class external extends external_api {
'showdeleterelatedaction' => new external_value(PARAM_BOOL, 'Whether to show the delete relation link or not')
));
}
/**
* Returns the description of external function parameters.
*
* @return external_function_parameters.
*/
public static function search_users_parameters() {
$query = new external_value(
PARAM_RAW,
'Query string'
);
$capability = new external_value(
PARAM_RAW,
'Required capability'
);
$limitfrom = new external_value(
PARAM_INT,
'Number of records to skip',
VALUE_DEFAULT,
0
);
$limitnum = new external_value(
PARAM_RAW,
'Number of records to fetch',
VALUE_DEFAULT,
100
);
return new external_function_parameters(array(
'query' => $query,
'capability' => $capability,
'limitfrom' => $limitfrom,
'limitnum' => $limitnum
));
}
public static function filter_users_with_capability_on_user_context_sql($capability,
$userid = 0,
$type=SQL_PARAMS_QM,
$prefix='param') {
global $USER, $DB;
$allresultsfilter = array('> 0', array());
$noresultsfilter = array('= -1', array());
if (empty($capability)) {
return $allresultsfilter;
}
if (!$capinfo = get_capability_info($capability)) {
throw new coding_exception('Capability does not exist: ' . $capability);
}
if (empty($userid)) {
$userid = $USER->id;
}
// Make sure the guest account and not-logged-in users never get any risky caps no matter what the actual settings are.
if (($capinfo->captype === 'write') or ($capinfo->riskbitmask & (RISK_XSS | RISK_CONFIG | RISK_DATALOSS))) {
if (isguestuser($userid) or $userid == 0) {
return $noresultsfilter;
}
}
if (is_siteadmin($userid)) {
// No filtering for site admins.
return $allresultsfilter;
}
// Check capability on system level.
$sql = 'SELECT
rc.id,
ctx.id contextid,
ctx.instanceid,
rc.roleid,
rc.capability,
rc.permission
FROM {role_capabilities} rc
JOIN {role_assignments} ra ON ra.contextid = rc.contextid
JOIN {context} ctx ON (ctx.id = rc.contextid)
WHERE ctx.contextlevel = :userlevel AND ra.userid = :userid';
$siterecords = $DB->get_records_sql($sql, array('userlevel' => CONTEXT_SYSTEM, 'userid' => $userid));
$userrecords = $DB->get_records_sql($sql, array('userlevel' => CONTEXT_USER, 'userid' => $userid));
$hassystem = false;
if (!empty($siterecords)) {
foreach ($siterecords as $record) {
if ($record->permission == CAP_PROHIBIT) {
return $noresultsfilter;
} else if ($record->permission == CAP_ALLOW) {
$hassystem = true;
}
}
}
if ($hassystem) {
// If allowed at system, search for roles prohibiting the capability at user context.
$excludeusers = array();
foreach ($userrecords as $record) {
if ($record->permission == CAP_PROHIBIT) {
$excludeusers[$record->instanceid] = $record->instanceid;
}
}
// Construct SQL excluding users with this role assigned for this user.
if (empty($excludeusers)) {
return $allresultsfilter;
}
list($sql, $params) = $DB->get_in_or_equal($excludeusers, $type, $prefix, false);
} else {
// If not allowed at system, search for roles allowing the capability at user context.
// Construct SQL excluding users with this role NOT assigned for this user.
$allowusers = array();
$prohibitusers = array();
foreach ($userrecords as $record) {
if ($record->permission == CAP_PROHIBIT) {
$prohibitusers[$record->instanceid] = $record->instanceid;
}
if ($record->permission == CAP_ALLOW) {
$allowusers[$record->instanceid] = $record->instanceid;
}
}
foreach ($prohibitusers as $userid => $userid2) {
unset($allowusers[$record->instanceid]);
}
if (empty($allowusers)) {
return $noresultsfilter;
}
list($sql, $params) = $DB->get_in_or_equal($allowusers, $type, $prefix);
}
// Return the goods!.
return array($sql, $params);
}
/**
* Search users.
*
* @param string $query
* @return array
*/
public static function search_users($query, $capability = '', $limitfrom = 0, $limitnum = 100) {
global $DB, $CFG, $PAGE, $USER;
$params = self::validate_parameters(self::search_users_parameters(),
array(
'query' => $query,
'capability' => $capability,
'limitfrom' => $limitfrom,
'limitnum' => $limitnum,
));
$query = $params['query'];
$cap = $params['capability'];
$limitfrom = $params['limitfrom'];
$limitnum = $params['limitnum'];
$context = context_system::instance();
self::validate_context($context);
list($filtercapsql, $filtercapparams) = self::filter_users_with_capability_on_user_context_sql($cap,
$USER->id,
SQL_PARAMS_NAMED);
$extrasearchfields = array();
if (!empty($CFG->showuseridentity) && has_capability('moodle/site:viewuseridentity', $context)) {
$extrasearchfields = explode(',', $CFG->showuseridentity);
}
$fields = \user_picture::fields('u', $extrasearchfields);
list($wheresql, $whereparams) = users_search_sql($query, 'u', true, $extrasearchfields);
list($sortsql, $sortparams) = users_order_by_sql('u', $query, $context);
$countsql = "SELECT COUNT('x') FROM {user} u WHERE $wheresql AND u.id $filtercapsql";
$countparams = $whereparams + $filtercapparams;
$sql = "SELECT $fields FROM {user} u WHERE $wheresql AND u.id $filtercapsql ORDER BY $sortsql";
$params = $whereparams + $filtercapparams + $sortparams;
$count = $DB->count_records_sql($countsql, $countparams);
$result = $DB->get_recordset_sql($sql, $params, $limitfrom, $limitnum);
$users = array();
foreach ($result as $key => $user) {
$newuser = array(
'id' => $user->id,
'fullname' => fullname($user)
);
// Add user picture.
$userpicture = new \user_picture($user);
$userpicture->size = 1; // Size f1.
$newuser['profileimageurl'] = $userpicture->get_url($PAGE)->out(false);
$userpicture->size = 0; // Size f2.
$newuser['profileimageurlsmall'] = $userpicture->get_url($PAGE)->out(false);
// Add identity fields.
foreach ($extrasearchfields as $field) {
$newuser[$field] = $user->$field;
}
$users[$key] = $newuser;
}
$result->close();
return array(
'users' => $users,
'count' => $count
);
}
/**
* Returns description of external function result value.
*
* @return external_description
*/
public static function search_users_returns() {
global $CFG;
require_once($CFG->dirroot . '/user/externallib.php');
return new external_single_structure(array(
'users' => new external_multiple_structure(new external_single_structure(array(
'id' => new external_value(PARAM_INT, 'User ID'),
'fullname' => new external_value(PARAM_NOTAGS, 'User full name'),
'profileimageurl' => new external_value(PARAM_URL, 'User picture URL', VALUE_OPTIONAL),
'profileimageurlsmall' => new external_value(PARAM_URL, 'Small user picture URL', VALUE_OPTIONAL),
'idnumber' => new external_value(PARAM_NOTAGS, 'ID number', VALUE_OPTIONAL),
'email' => new external_value(PARAM_TEXT, 'Email', VALUE_OPTIONAL),
'phone1' => new external_value(PARAM_NOTAGS, 'Phone 1', VALUE_OPTIONAL),
'phone2' => new external_value(PARAM_NOTAGS, 'Phone 2', VALUE_OPTIONAL),
'department' => new external_value(PARAM_TEXT, 'Department', VALUE_OPTIONAL),
'institution' => new external_value(PARAM_TEXT, 'Institution', VALUE_OPTIONAL),
))),
'count' => new external_value(PARAM_INT, 'Total number of results.')
));
}
}
+9 -1
View File
@@ -559,6 +559,14 @@ $functions = array(
'capabilities' => 'tool/lp:competencyread',
'ajax' => true,
),
'tool_lp_search_users' => array(
'classname' => 'tool_lp\external',
'methodname' => 'search_users',
'classpath' => '',
'description' => 'Search for users.',
'type' => 'read',
'capabilities' => '',
'ajax' => true,
),
);
@@ -0,0 +1,44 @@
{{!
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/form_autocomplete_suggestions
Moodle template for the list of valid options in an autocomplate form element.
Classes required for JS:
* none
Data attributes required for JS:
* none
Context variables required for this template:
* suggestionsId The dom id of the current suggestions list.
* options List of options with label and value fields.
Example context (json):
{ "suggestionsId": 1, "options": [
{ "label": "Item label with <strong>tags</strong>", "value": "5" },
{ "label": "Another item label with <strong>tags</strong>", "value": "4" }
]}
}}
<span>
<img height="18" src="{{profileimageurlsmall}}" alt="" role="presentation">
<span>{{fullname}}</span>
{{#hasidentity}}
<span><small>{{identity}}</small></span>
{{/hasidentity}}
</span>
+123
View File
@@ -2497,4 +2497,127 @@ class tool_lp_external_testcase extends externallib_advanced_testcase {
$this->assertEquals(2, $c2a->get_sortorder());
}
public function test_search_users() {
global $CFG;
$this->resetAfterTest(true);
$dg = $this->getDataGenerator();
$ux = $dg->create_user();
$u1 = $dg->create_user(array('idnumber' => 'Cats', 'firstname' => 'Bob', 'lastname' => 'Dylan',
'email' => '[email protected]', 'phone1' => '123456', 'phone2' => '78910', 'department' => 'Marketing',
'institution' => 'HQ'));
$u2 = $dg->create_user(array('idnumber' => 'Dogs', 'firstname' => 'Alice', 'lastname' => 'Dylan',
'email' => '[email protected]', 'phone1' => '33333', 'phone2' => '77777', 'department' => 'Development',
'institution' => 'O2'));
$u3 = $dg->create_user(array('idnumber' => 'Fish', 'firstname' => 'Thomas', 'lastname' => 'Xow',
'email' => '[email protected]', 'phone1' => '77777', 'phone2' => '33333', 'department' => 'Research',
'institution' => 'Bob'));
// We need to give the user the capability we are searching for on each of the test users.
$this->setAdminUser();
$usercontext = context_user::instance($u1->id);
$dummyrole = $this->assignUserCapability('tool/lp:planmanage', $usercontext->id);
$usercontext = context_user::instance($u2->id);
$this->assignUserCapability('tool/lp:planmanage', $usercontext->id, $dummyrole);
$usercontext = context_user::instance($u3->id);
$this->assignUserCapability('tool/lp:planmanage', $usercontext->id, $dummyrole);
$this->setUser($ux);
$usercontext = context_user::instance($u1->id);
$this->assignUserCapability('tool/lp:planmanage', $usercontext->id, $dummyrole);
$usercontext = context_user::instance($u2->id);
$this->assignUserCapability('tool/lp:planmanage', $usercontext->id, $dummyrole);
$usercontext = context_user::instance($u3->id);
$this->assignUserCapability('tool/lp:planmanage', $usercontext->id, $dummyrole);
$this->setAdminUser();
// No identity fields.
$CFG->showuseridentity = '';
$result = external::search_users('cats', 'tool/lp:planmanage');
$result = external_api::clean_returnvalue(external::search_users_returns(), $result);
$this->assertCount(0, $result['users']);
$this->assertEquals(0, $result['count']);
// Filter by name.
$CFG->showuseridentity = '';
$result = external::search_users('dylan', 'tool/lp:planmanage');
$result = external_api::clean_returnvalue(external::search_users_returns(), $result);
$this->assertCount(2, $result['users']);
$this->assertEquals(2, $result['count']);
$this->assertEquals($u2->id, $result['users'][0]['id']);
$this->assertEquals($u1->id, $result['users'][1]['id']);
// Filter by institution and name.
$CFG->showuseridentity = 'institution';
$result = external::search_users('bob', 'tool/lp:planmanage');
$result = external_api::clean_returnvalue(external::search_users_returns(), $result);
$this->assertCount(2, $result['users']);
$this->assertEquals(2, $result['count']);
$this->assertEquals($u1->id, $result['users'][0]['id']);
$this->assertEquals($u3->id, $result['users'][1]['id']);
// Filter by id number.
$CFG->showuseridentity = 'idnumber';
$result = external::search_users('cats', 'tool/lp:planmanage');
$result = external_api::clean_returnvalue(external::search_users_returns(), $result);
$this->assertCount(1, $result['users']);
$this->assertEquals(1, $result['count']);
$this->assertEquals($u1->id, $result['users'][0]['id']);
$this->assertEquals($u1->idnumber, $result['users'][0]['idnumber']);
$this->assertArrayNotHasKey('email', $result['users'][0]);
$this->assertArrayNotHasKey('phone1', $result['users'][0]);
$this->assertArrayNotHasKey('phone2', $result['users'][0]);
$this->assertArrayNotHasKey('department', $result['users'][0]);
$this->assertArrayNotHasKey('institution', $result['users'][0]);
// Filter by email.
$CFG->showuseridentity = 'email';
$result = external::search_users('y', 'tool/lp:planmanage');
$result = external_api::clean_returnvalue(external::search_users_returns(), $result);
$this->assertCount(3, $result['users']);
$this->assertEquals(3, $result['count']);
$this->assertEquals($u2->id, $result['users'][0]['id']);
$this->assertEquals($u2->email, $result['users'][0]['email']);
$this->assertEquals($u1->id, $result['users'][1]['id']);
$this->assertEquals($u1->email, $result['users'][1]['email']);
$this->assertEquals($u3->id, $result['users'][2]['id']);
$this->assertEquals($u3->email, $result['users'][2]['email']);
// Filter by any.
$CFG->showuseridentity = 'idnumber,email,phone1,phone2,department,institution';
$result = external::search_users('y', 'tool/lp:planmanage');
$result = external_api::clean_returnvalue(external::search_users_returns(), $result);
$this->assertCount(3, $result['users']);
$this->assertEquals(3, $result['count']);
$this->assertArrayHasKey('idnumber', $result['users'][0]);
$this->assertArrayHasKey('email', $result['users'][0]);
$this->assertArrayHasKey('phone1', $result['users'][0]);
$this->assertArrayHasKey('phone2', $result['users'][0]);
$this->assertArrayHasKey('department', $result['users'][0]);
$this->assertArrayHasKey('institution', $result['users'][0]);
// Switch to a user that cannot view identity fields.
$this->setUser($ux);
$CFG->showuseridentity = 'idnumber,email,phone1,phone2,department,institution';
// Only names are included.
$result = external::search_users('fish');
$result = external_api::clean_returnvalue(external::search_users_returns(), $result);
$this->assertCount(0, $result['users']);
$this->assertEquals(0, $result['count']);
$result = external::search_users('bob', 'tool/lp:planmanage');
$result = external_api::clean_returnvalue(external::search_users_returns(), $result);
$this->assertCount(1, $result['users']);
$this->assertEquals(1, $result['count']);
$this->assertEquals($u1->id, $result['users'][0]['id']);
$this->assertArrayNotHasKey('idnumber', $result['users'][0]);
$this->assertArrayNotHasKey('email', $result['users'][0]);
$this->assertArrayNotHasKey('phone1', $result['users'][0]);
$this->assertArrayNotHasKey('phone2', $result['users'][0]);
$this->assertArrayNotHasKey('department', $result['users'][0]);
$this->assertArrayNotHasKey('institution', $result['users'][0]);
}
}