]> git.mjollnir.org Git - moodle.git/commitdiff
user selection: MDL-16996 Improve the user selector used on the assign roles and...
authortjhunt <tjhunt>
Tue, 28 Oct 2008 06:51:36 +0000 (06:51 +0000)
committertjhunt <tjhunt>
Tue, 28 Oct 2008 06:51:36 +0000 (06:51 +0000)
lang/en_utf8/error.php
user/selector/lib.php
user/selector/script.js [new file with mode: 0644]
user/selector/search.php
user/selector/test.php

index bae343195997d5304d08dc2e8e3e0d0c080840d7..c724a69b4ce30472b4afe5b6319061c897041952 100644 (file)
@@ -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';
index e43300023366a0ad6a42b6dd1a3101e18ee83d52..c202f971746b18b7ff05a9a702d12b38053af101 100644 (file)
@@ -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 = '<div class="userselector" id="' . $this->name . '_wrapper">' . "\n" .
                 '<select name="' . $name . '" id="' . $this->name . '" ' .
                 $multiselect . 'size="' . $this->rows . '">' . "\n";
-        foreach ($groupedusers as $groupname => $users) {
-            $output .= $this->output_optgroup($groupname, $users, $select);
-        }
-        if (!empty($this->selected)) {
-            $output .= $this->output_optgroup(get_string('previouslyselectedusers'), $this->selected, true);
-        }
+
+        // Populate the select.
+        $output .= $this->output_options($groupedusers, $select);
 
         // Output the search controls.
         $output .= "</select>\n<div>\n";
@@ -161,16 +155,8 @@ abstract class user_selector_base {
                 $this->name . '_searchbutton" value="' . $this->search_button_caption() . '" />';
         $output .= "</div>\n</div>\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 .=  '<p><a href="' . $CFG->wwwroot . '/user/selector/search.php?selectorid=' .
-                $hash . '&amp;' . 'sesskey=' . sesskey() . '&amp;search=">Ajax search script</a></p>'; // 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 <optgroup>s and <options>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 = '<optgroup label="' . s($groupname) . ' (' . count($users) . ')">' . "\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 (file)
index 0000000..fe1c5e1
--- /dev/null
@@ -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
index 13a7caf7400487af43e904a481d8ab295a9e755b..3a269727ac33ed1f073270db4c4ffeb0858f03f4 100644 (file)
@@ -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
index 2d19161a613f810b02f129b6d975318f7850cd28..a5a22db63d198cf14f547e23776473c7a4bd12a4 100644 (file)
@@ -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');