diff --git a/admin/tool/lp/amd/build/form-user-selector.min.js b/admin/tool/lp/amd/build/form-user-selector.min.js
new file mode 100644
index 00000000000..8c158e8f3b5
--- /dev/null
+++ b/admin/tool/lp/amd/build/form-user-selector.min.js
@@ -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)}}});
\ No newline at end of file
diff --git a/admin/tool/lp/amd/src/form-user-selector.js b/admin/tool/lp/amd/src/form-user-selector.js
new file mode 100644
index 00000000000..0122fe936cb
--- /dev/null
+++ b/admin/tool/lp/amd/src/form-user-selector.js
@@ -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 .
+
+/**
+ * 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);
+ }
+
+ };
+
+});
diff --git a/admin/tool/lp/classes/external.php b/admin/tool/lp/classes/external.php
index de8785b187c..25b8c01df7a 100644
--- a/admin/tool/lp/classes/external.php
+++ b/admin/tool/lp/classes/external.php
@@ -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.')
+ ));
+ }
+
}
diff --git a/admin/tool/lp/db/services.php b/admin/tool/lp/db/services.php
index af85bb7dd20..9d763140269 100644
--- a/admin/tool/lp/db/services.php
+++ b/admin/tool/lp/db/services.php
@@ -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,
+ ),
);
diff --git a/admin/tool/lp/templates/form-user-selector-suggestion.mustache b/admin/tool/lp/templates/form-user-selector-suggestion.mustache
new file mode 100644
index 00000000000..95fb8138246
--- /dev/null
+++ b/admin/tool/lp/templates/form-user-selector-suggestion.mustache
@@ -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 .
+}}
+{{!
+ @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 tags", "value": "5" },
+ { "label": "Another item label with tags", "value": "4" }
+ ]}
+}}
+
+
+ {{fullname}}
+ {{#hasidentity}}
+ {{identity}}
+ {{/hasidentity}}
+
diff --git a/admin/tool/lp/tests/externallib_test.php b/admin/tool/lp/tests/externallib_test.php
index e19a852885c..b3c752604c2 100644
--- a/admin/tool/lp/tests/externallib_test.php
+++ b/admin/tool/lp/tests/externallib_test.php
@@ -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' => 'bobby@dylan.com', 'phone1' => '123456', 'phone2' => '78910', 'department' => 'Marketing',
+ 'institution' => 'HQ'));
+ $u2 = $dg->create_user(array('idnumber' => 'Dogs', 'firstname' => 'Alice', 'lastname' => 'Dylan',
+ 'email' => 'alyson@dylan.com', 'phone1' => '33333', 'phone2' => '77777', 'department' => 'Development',
+ 'institution' => 'O2'));
+ $u3 = $dg->create_user(array('idnumber' => 'Fish', 'firstname' => 'Thomas', 'lastname' => 'Xow',
+ 'email' => 'fishy@moodle.com', '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]);
+ }
+
}