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]); + } + }