From d56f9e659dd921157435ad4e4e68574a687af382 Mon Sep 17 00:00:00 2001 From: tjhunt Date: Tue, 28 Oct 2008 06:51:36 +0000 Subject: [PATCH] user selection: MDL-16996 Improve the user selector used on the assign roles and group memebers pages - Write the JavaScript to do the Ajax requests and update the list of users. --- lang/en_utf8/error.php | 2 + user/selector/lib.php | 91 +++++++++---- user/selector/script.js | 270 +++++++++++++++++++++++++++++++++++++++ user/selector/search.php | 12 +- user/selector/test.php | 5 +- 5 files changed, 356 insertions(+), 24 deletions(-) create mode 100644 user/selector/script.js diff --git a/lang/en_utf8/error.php b/lang/en_utf8/error.php index bae3431959..c724a69b4c 100644 --- a/lang/en_utf8/error.php +++ b/lang/en_utf8/error.php @@ -316,6 +316,7 @@ $string['modulemissingcode'] = 'Module $a is missing the code needed to perform $string['modulerequirementsnotmet'] = 'Module \"$a->modulename\" ($a->moduleversion) could not be installed. It requires a newer version of Moodle (currently you are using $a->currentmoodle, you need $a->requiremoodle).'; $string['mustbeteacher'] = 'You must be a teacher to look at this page'; $string['multiplerestorenotallow'] = 'Multiple restore execution not allowed!'; +$string['mustbeloggedin'] = 'You must be logged in to do this'; $string['needphpext'] = 'You need to add $a support to your PHP installation'; $string['needcopy'] = 'You need to copy something first!'; $string['needcoursecategroyid'] = 'Either course id or category must be specified'; @@ -410,6 +411,7 @@ $string['unknownhelp'] = 'Unknown help topic $a'; $string['unknowngroup'] = 'Unknown group \"$a\"'; $string['unknownrole'] = 'Unknown role \"$a\"'; $string['unknownuseraction'] = 'Sorry, I do not understand this user action'; +$string['unknownuserselector'] = 'Unknown user selector'; $string['unknownmodulename'] = 'Unknown module name for form'; $string['unknoworder'] = 'Unknown ordering'; $string['unknowparamtype'] = 'Unknown parameter type: $a'; diff --git a/user/selector/lib.php b/user/selector/lib.php index e433000233..c202f97174 100644 --- a/user/selector/lib.php +++ b/user/selector/lib.php @@ -123,9 +123,6 @@ abstract class user_selector_base { public function display($return = false) { global $USER, $CFG; - // Ensure that the list of previously selected users is up to date. - $this->get_selected_users(); - // Get the list of requested users, and if there is only one, set a flag to autoselect it. $search = optional_param($this->name . '_searchtext', '', PARAM_RAW); $groupedusers = $this->find_users($search); @@ -146,12 +143,9 @@ abstract class user_selector_base { $output = '
' . "\n" . '\n
\n"; @@ -161,16 +155,8 @@ abstract class user_selector_base { $this->name . '_searchbutton" value="' . $this->search_button_caption() . '" />'; $output .= "
\n
\n\n"; - // This method trashes $this->selected, so reset it so if someone tries to - // Use it again, it is rebuilt. - $this->selected = null; - - // Put the options into the session for the benefit of the ajax code. - $options = $this->get_options(); - $hash = md5(serialize($options)); - $USER->userselectors[$hash] = $options; - $output .= '

Ajax search script

'; // DONOTCOMMIT + // Initialise the ajax functionality. + $output .= $this->initialise_javascript(); // Return or output it. if ($return) { @@ -323,6 +309,38 @@ abstract class user_selector_base { return array(implode(' AND ', $tests), $params); } + /** + * Output the list of s and s that go inside the select. + * This method should do the same as the JavaScript method + * user_selector.prototype.handle_response. + * + * @param unknown_type $groupedusers + * @param unknown_type $select + * @return unknown + */ + protected function output_options($groupedusers, $select) { + $output = ''; + + // Ensure that the list of previously selected users is up to date. + $this->get_selected_users(); + + // Output each optgroup. + foreach ($groupedusers as $groupname => $users) { + $output .= $this->output_optgroup($groupname, $users, $select); + } + + // If there were previously selected users who do not match the search, show them too. + if (!empty($this->selected)) { + $output .= $this->output_optgroup(get_string('previouslyselectedusers'), $this->selected, true); + } + + // This method trashes $this->selected, so clear the cache so it is + // rebuilt before anyone tried to use it again. + $this->selected = null; + + return $output; + } + protected function output_optgroup($groupname, $users, $select) { $output = '' . "\n"; if (!empty($users)) { @@ -344,10 +362,10 @@ abstract class user_selector_base { } /** - * Convert a user object to a string suitable for displaying as an option in the dropdown. + * Convert a user object to a string suitable for displaying as an option in the list box. * * @param object $user the user to display. - * @return string a string representation to display. + * @return string a string representation of the user. */ protected function output_user($user) { $bits = array( @@ -365,9 +383,38 @@ abstract class user_selector_base { protected function search_button_caption() { return get_string('search'); } + + /** + * Enter description here... + * + */ + protected function initialise_javascript() { + global $USER; + $output = ''; + + // Required JavaScript code. + require_js(array('yui_yahoo', 'yui_event', 'yui_json', 'yui_connection', 'yui_datasource')); + require_js('user/selector/script.js'); + + // Put the options into the session, to allow search.php to respond to the ajax requests. + $options = $this->get_options(); + $hash = md5(serialize($options)); + $USER->userselectors[$hash] = $options; + + // Initialise the selector. + $output .= print_js_call('new user_selector', array($this->name, $hash, + sesskey(), $this->extrafields, get_string('previouslyselectedusers')), true); + return $output; + } +} + +class role_assign_potential_user_selector extends user_selector_base { + public function find_users($search) { + return array(); // TODO + } } -class role_assign_user_selector extends user_selector_base { +class role_assign_current_user_selector extends user_selector_base { public function find_users($search) { return array(); // TODO } diff --git a/user/selector/script.js b/user/selector/script.js new file mode 100644 index 0000000000..fe1c5e17c9 --- /dev/null +++ b/user/selector/script.js @@ -0,0 +1,270 @@ +// JavaScript for the user selectors. +// This is somewhat inspired by the autocomplete component in YUI. +// license: http://www.gnu.org/copyleft/gpl.html GNU Public License +// package: userselector + +/** + * + * @constructor + */ +function user_selector(name, hash, sesskey, extrafields, strprevselected) { + this.name = name; + this.extrafields = extrafields; + this.strprevselected = strprevselected; + + // Set up the data source. + this.datasource = new YAHOO.util.XHRDataSource(moodle_cfg.wwwroot + + '/user/selector/search.php?selectorid=' + hash + '&sesskey=' + sesskey + '&search='); + this.datasource.connXhrMode = 'cancelStaleRequests'; + this.datasource.responseType = YAHOO.util.XHRDataSource.TYPE_JSON; + this.datasource.responseSchema = {resultsList: 'results'}; + + // Find some key HTML elements. + this.searchfield = document.getElementById(this.name + '_searchtext'); + this.listbox = document.getElementById(this.name); + + // Hide the search button and replace it with a label. + var searchbutton = document.getElementById(this.name + '_searchbutton'); + var label = document.createElement('label'); + label.for = this.name + '_searchtext'; + label.appendChild(document.createTextNode(searchbutton.value)); + this.searchfield.parentNode.insertBefore(label, this.searchfield); + searchbutton.parentNode.removeChild(searchbutton); + + // Hook up the event handler. + var oself = this; + YAHOO.util.Event.addListener(this.searchfield, "keyup", function(e) { oself.handle_keyup() }); + this.lastsearch = this.get_search_text(); +} + +/** + * This id/name used for this control in the HTML. + * @property name + * @type String + */ +user_selector.prototype.name = null; + +/** + * Array of fields to display for each user, in addition to fullname. + * @property extrafields + * @type Array + */ +user_selector.prototype.extrafields = []; + +/** + * The datasource used to fetch lists of users from Moodle. + * @property datasource + * @type YAHOO.widget.DataSource + */ +user_selector.prototype.datasource = null; + +/** + * Number of seconds to delay before submitting a query request. If a query + * request is received before a previous one has completed its delay, the + * previous request is cancelled and the new request is set to the delay. + * + * @property querydelay + * @type Number + * @default 0.2 + */ +user_selector.prototype.querydelay = 0.2; + +/** + * The input element that contains the search term. + * + * @property searchfield + * @type HTMLInputElement + */ +user_selector.prototype.searchfield = 0.2; + +/** + * The select element that contains the list of users. + * + * @property listbox + * @type HTMLSelectElement + */ +user_selector.prototype.listbox = null; + +/** + * Used to hold the timeout id of the timeout that waits before doing a search. + * + * @property timeoutid + * @type Number + */ +user_selector.prototype.timeoutid = null; + +/** + * The last string that we searched for. + * + * @property lastsearch + * @type String + */ +user_selector.prototype.lastsearch = null; + +/** + * Name of the previously selected users group. + * + * @property strprevselected + * @type String + */ +user_selector.prototype.strprevselected = ''; + +/** + * Used to track whether there is only one optoin matchin the search results, if + * so, it is automatically selected. + * + * @property strprevselected + * @type Object + **/ +user_selector.prototype.onlyoption = null; + +/** + * Key up hander for the search text box. Trigger an ajax search after a delay. + */ +user_selector.prototype.handle_keyup = function() { + if (this.timeoutid) { + clearTimeout(this.timeoutid); + this.timeoutid = null; + } + var oself = this; + this.timeoutid = setTimeout(function() { oself.send_query() }, this.querydelay * 1000); +} + +/** + * @return String the value to search for, with leading and trailing whitespace trimmed. + */ +user_selector.prototype.get_search_text = function() { + return this.searchfield.value.replace(/^ +| +$/, ''); +} + +/** + * Fires off the ajax search request. + */ +user_selector.prototype.send_query = function() { + var value = this.get_search_text(); + if (this.lastsearch == value) { + return; + } + this.datasource.sendRequest(this.searchfield.value, { + success: this.handle_response, + failure: this.handle_failure, + scope: this + }); + this.lastsearch = value; + this.listbox.style.background = 'url(' + moodle_cfg.pixpath + '/i/loading.gif) no-repeat center center'; +} + +/** + * Handle what happens when we get some data back from the search. + * @param Object request not used. + * @param Object data the list of users that was returned. + */ +user_selector.prototype.handle_response = function(request, data) { + this.listbox.style.background = ''; + this.output_options(data); +} + +/** + * Handles what happens when the ajax request fails. + */ +user_selector.prototype.handle_failure = function() { + this.listbox.style.background = ''; +} + +/** + * This method should do the same sort of thing as the PHP method + * user_selector_base::output_options. + * @param object data the list of users to populate the list box with. + */ +user_selector.prototype.output_options = function(data) { + // Clear out the existing options, keeping any ones that are already selected. + this.selected = {}; + var groups = this.listbox.getElementsByTagName('optgroup'); + while (groups.length > 0) { + var optgroup = groups[0]; // Remeber that groups is a live array as we remove optgroups from the select, it updates. + var options = optgroup.getElementsByTagName('option'); + while (options.length > 0) { + var option = options[0]; + if (option.selected) { + var optiontext = option.innerText || option.textContent + this.selected[option.value] = { id: option.value, formatted: optiontext }; + } + optgroup.removeChild(option); + } + this.listbox.removeChild(optgroup); + } + + var results = data.results[0]; + + // Output each optgroup. + this.onlyoption = null; + for (groupname in results) { + this.output_group(groupname, results[groupname], false); + } + + // If there was only one option matching the search results, select it. + if (this.onlyoption) { + this.onlyoption.selected = true; + } + this.onlyoption = null; + + // If there were previously selected users who do not match the search, show them too. + var areprevselected = false; + for (user in this.selected) { + areprevselected = true; + break; + } + if (areprevselected) { + this.output_group(this.strprevselected, this.selected, true); + } + this.selected = null; + +} + +user_selector.prototype.output_group = function(groupname, users, select) { + var optgroup = document.createElement('optgroup'); + optgroup.label = groupname; + var count = 0; + for (var userid in users) { + var user = users[userid]; + var option = document.createElement('option'); + option.value = user.id; + option.appendChild(document.createTextNode(this.output_user(user))); + if (select || this.selected[user.id]) { + option.selected = 'selected'; + } + delete this.selected[user.id]; + optgroup.appendChild(option); + if (this.onlyoption === null) { + this.onlyoption = option; + } else { + this.onlyoption = false; + } + count++; + } + if (count == 0) { + var option = document.createElement('option'); + option.disabled = 'disabled'; + option.appendChild(document.createTextNode('\u00A0')); + optgroup.appendchild(option); + } + optgroup.label += ' (' + count + ')'; + this.listbox.appendChild(optgroup); +} + +/** + * Convert a user object to a string suitable for displaying as an option in the list box. + * + * @param Object user the user to display. + * @return string a string representation of the user. + */ +user_selector.prototype.output_user = function(user) { + if (user.formatted) { + return user.formatted; + } + var output = user.fullname; + for (var i = 0; i < this.extrafields.length; i++) { + output += ', ' + user[this.extrafields[i]]; + } + return output; +} \ No newline at end of file diff --git a/user/selector/search.php b/user/selector/search.php index 13a7caf740..3a269727ac 100644 --- a/user/selector/search.php +++ b/user/selector/search.php @@ -34,7 +34,9 @@ require_once(dirname(__FILE__) . '/../../config.php'); require_once($CFG->dirroot . '/user/selector/lib.php'); // Check access. -require_login(); +if (!isloggedin()) {; + print_error('mustbeloggedin'); +} if (!confirm_sesskey()) { print_error('invalidsesskey'); } @@ -62,5 +64,13 @@ $userselector = new $classname($name, $options); // Do the search and output the results. $users = $userselector->find_users($search); +foreach ($users as &$group) { + foreach ($group as &$user) { + $user->fullname = fullname($user); + } +} + + +header('Content-type: application/json'); echo json_encode(array('results' => $users)); ?> \ No newline at end of file diff --git a/user/selector/test.php b/user/selector/test.php index 2d19161a61..a5a22db63d 100644 --- a/user/selector/test.php +++ b/user/selector/test.php @@ -14,7 +14,8 @@ class test_user_selector extends user_selector_base { list($wherecondition, $params) = $this->search_sql($search, 'u'); $sql = 'SELECT ' . $this->required_fields_sql('u') . ' FROM {user} u' . - ' WHERE ' . $wherecondition; + ' WHERE ' . $wherecondition . + ' ORDER BY u.lastname, u.firstname'; $users = $DB->get_recordset_sql($sql, $params); $groupedusers = array(); if ($search) { @@ -40,6 +41,8 @@ if ($justdefineclass) { return; } +require_login(); +require_capability('moodle/site:config', get_context_instance(CONTEXT_SYSTEM)); print_header(); $userselector = new test_user_selector('myuserselector'); -- 2.39.5