f9c8717c72329997b0722f732dd2b74013684d32
[moodle.git] / user / selector / lib.php
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
18 /**
19  * Code for ajax user selectors.
20  *
21  * @package   user
22  * @copyright 1999 onwards Martin Dougiamas  http://dougiamas.com
23  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
26 /**
27  * The default size of a user selector.
28  */
29 define('USER_SELECTOR_DEFAULT_ROWS', 20);
31 /**
32  * Base class for user selectors.
33  *
34  * In your theme, you must give each user-selector a defined width. If the
35  * user selector has name="myid", then the div myid_wrapper must have a width
36  * specified.
37  */
38 abstract class user_selector_base {
39     /** @var string The control name (and id) in the HTML. */
40     protected $name;
41     /** @var array Extra fields to search on and return in addition to firstname and lastname. */
42     protected $extrafields;
43     /** @var boolean Whether the conrol should allow selection of many users, or just one. */
44     protected $multiselect = true;
45     /** @var int The height this control should have, in rows. */
46     protected $rows = USER_SELECTOR_DEFAULT_ROWS;
47     /** @var array A list of userids that should not be returned by this control. */
48     protected $exclude = array();
49     /** @var array|null A list of the users who are selected. */
50     protected $selected = null;
51     /** @var boolean When the search changes, do we keep previously selected options that do
52      * not match the new search term? */
53     protected $preserveselected = false;
54     /** @var boolean If only one user matches the search, should we select them automatically. */
55     protected $autoselectunique = false;
56     /** @var boolean When searching, do we only match the starts of fields (better performance)
57      * or do we match occurrences anywhere? */
58     protected $searchanywhere = false;
59     /** @var mixed This is used by get selected users */
60     protected $validatinguserids = null;
62     /**  @var boolean Used to ensure we only output the search options for one user selector on
63      * each page. */
64     private static $searchoptionsoutput = false;
66     /** @var array JavaScript YUI3 Module definition */
67     protected static $jsmodule = array(
68                 'name' => 'user_selector',
69                 'fullpath' => '/user/selector/module.js',
70                 'requires'  => array('node', 'event-custom', 'datasource', 'json'),
71                 'strings' => array(
72                     array('previouslyselectedusers', 'moodle', '%%SEARCHTERM%%'),
73                     array('nomatchingusers', 'moodle', '%%SEARCHTERM%%'),
74                     array('none', 'moodle')
75                 ));
78     // Public API ==============================================================
80     /**
81      * Constructor. Each subclass must have a constructor with this signature.
82      *
83      * @param string $name the control name/id for use in the HTML.
84      * @param array $options other options needed to construct this selector.
85      * You must be able to clone a userselector by doing new get_class($us)($us->get_name(), $us->get_options());
86      */
87     public function __construct($name, $options = array()) {
88         global $CFG, $PAGE;
90         // Initialise member variables from constructor arguments.
91         $this->name = $name;
92         if (isset($options['extrafields'])) {
93             $this->extrafields = $options['extrafields'];
94         } else if (!empty($CFG->extrauserselectorfields)) {
95             $this->extrafields = explode(',', $CFG->extrauserselectorfields);
96         } else {
97             $this->extrafields = array();
98         }
99         if (isset($options['exclude']) && is_array($options['exclude'])) {
100             $this->exclude = $options['exclude'];
101         }
102         if (isset($options['multiselect'])) {
103             $this->multiselect = $options['multiselect'];
104         }
106         // Read the user prefs / optional_params that we use.
107         $this->preserveselected = $this->initialise_option('userselector_preserveselected', $this->preserveselected);
108         $this->autoselectunique = $this->initialise_option('userselector_autoselectunique', $this->autoselectunique);
109         $this->searchanywhere = $this->initialise_option('userselector_searchanywhere', $this->searchanywhere);
110     }
112     /**
113      * All to the list of user ids that this control will not select. For example,
114      * on the role assign page, we do not list the users who already have the role
115      * in question.
116      *
117      * @param array $arrayofuserids the user ids to exclude.
118      */
119     public function exclude($arrayofuserids) {
120         $this->exclude = array_unique(array_merge($this->exclude, $arrayofuserids));
121     }
123     /**
124      * Clear the list of excluded user ids.
125      */
126     public function clear_exclusions() {
127         $exclude = array();
128     }
130     /**
131      * @return array the list of user ids that this control will not select.
132      */
133     public function get_exclusions() {
134         return clone($this->exclude);
135     }
137     /**
138      * @return array of user objects. The users that were selected. This is a more sophisticated version
139      * of optional_param($this->name, array(), PARAM_INTEGER) that validates the
140      * returned list of ids against the rules for this user selector.
141      */
142     public function get_selected_users() {
143         // Do a lazy load.
144         if (is_null($this->selected)) {
145             $this->selected = $this->load_selected_users();
146         }
147         return $this->selected;
148     }
150     /**
151      * Convenience method for when multiselect is false (throws an exception if not).
152      * @return object the selected user object, or null if none.
153      */
154     public function get_selected_user() {
155         if ($this->multiselect) {
156             throw new moodle_exception('cannotcallusgetselecteduser');
157         }
158         $users = $this->get_selected_users();
159         if (count($users) == 1) {
160             return reset($users);
161         } else if (count($users) == 0) {
162             return null;
163         } else {
164             throw new moodle_exception('userselectortoomany');
165         }
166     }
168     /**
169      * If you update the database in such a way that it is likely to change the
170      * list of users that this component is allowed to select from, then you
171      * must call this method. For example, on the role assign page, after you have
172      * assigned some roles to some users, you should call this.
173      */
174     public function invalidate_selected_users() {
175         $this->selected = null;
176     }
178     /**
179      * Output this user_selector as HTML.
180      * @param boolean $return if true, return the HTML as a string instead of outputting it.
181      * @return mixed if $return is true, returns the HTML as a string, otherwise returns nothing.
182      */
183     public function display($return = false) {
184         global $PAGE;
186         // Get the list of requested users.
187         $search = optional_param($this->name . '_searchtext', '', PARAM_RAW);
188         if (optional_param($this->name . '_clearbutton', false, PARAM_BOOL)) {
189             $search = '';
190         }
191         $groupedusers = $this->find_users($search);
193         // Output the select.
194         $name = $this->name;
195         $multiselect = '';
196         if ($this->multiselect) {
197             $name .= '[]';
198             $multiselect = 'multiple="multiple" ';
199         }
200         $output = '<div class="userselector" id="' . $this->name . '_wrapper">' . "\n" .
201                 '<select name="' . $name . '" id="' . $this->name . '" ' .
202                 $multiselect . 'size="' . $this->rows . '">' . "\n";
204         // Populate the select.
205         $output .= $this->output_options($groupedusers, $search);
207         // Output the search controls.
208         $output .= "</select>\n<div>\n";
209         $output .= '<input type="text" name="' . $this->name . '_searchtext" id="' .
210                 $this->name . '_searchtext" size="15" value="' . s($search) . '" />';
211         $output .= '<input type="submit" name="' . $this->name . '_searchbutton" id="' .
212                 $this->name . '_searchbutton" value="' . $this->search_button_caption() . '" />';
213         $output .= '<input type="submit" name="' . $this->name . '_clearbutton" id="' .
214                 $this->name . '_clearbutton" value="' . get_string('clear') . '" />';
216         // And the search options.
217         $optionsoutput = false;
218         if (!user_selector_base::$searchoptionsoutput) {
219             $output .= print_collapsible_region_start('', 'userselector_options',
220                     get_string('searchoptions'), 'userselector_optionscollapsed', true, true);
221             $output .= $this->option_checkbox('preserveselected', $this->preserveselected, get_string('userselectorpreserveselected'));
222             $output .= $this->option_checkbox('autoselectunique', $this->autoselectunique, get_string('userselectorautoselectunique'));
223             $output .= $this->option_checkbox('searchanywhere', $this->searchanywhere, get_string('userselectorsearchanywhere'));
224             $output .= print_collapsible_region_end(true);
226             $PAGE->requires->js_init_call('M.core_user.init_user_selector_options_tracker', array(), false, self::$jsmodule);
227             user_selector_base::$searchoptionsoutput = true;
228         }
229         $output .= "</div>\n</div>\n\n";
231         // Initialise the ajax functionality.
232         $output .= $this->initialise_javascript($search);
234         // Return or output it.
235         if ($return) {
236             return $output;
237         } else {
238             echo $output;
239         }
240     }
242     /**
243      * The height this control will be displayed, in rows.
244      *
245      * @param integer $numrows the desired height.
246      */
247     public function set_rows($numrows) {
248         $this->rows = $numrows;
249     }
251     /**
252      * @return integer the height this control will be displayed, in rows.
253      */
254     public function get_rows() {
255         return $this->rows;
256     }
258     /**
259      * Whether this control will allow selection of many, or just one user.
260      *
261      * @param boolean $multiselect true = allow multiple selection.
262      */
263     public function set_multiselect($multiselect) {
264         $this->multiselect = $multiselect;
265     }
267     /**
268      * @return boolean whether this control will allow selection of more than one user.
269      */
270     public function is_multiselect() {
271         return $this->multiselect;
272     }
274     /**
275      * @return string the id/name that this control will have in the HTML.
276      */
277     public function get_name() {
278         return $this->name;
279     }
281     /**
282      * Set the user fields that are displayed in the selector in addition to the
283      * user's name.
284      *
285      * @param array $fields a list of field names that exist in the user table.
286      */
287     public function set_extra_fields($fields) {
288         $this->extrafields = $fields;
289     }
291     // API for sublasses =======================================================
293     /**
294      * Search the database for users matching the $search string, and any other
295      * conditions that apply. The SQL for testing whether a user matches the
296      * search string should be obtained by calling the search_sql method.
297      *
298      * This method is used both when getting the list of choices to display to
299      * the user, and also when validating a list of users that was selected.
300      *
301      * When preparing a list of users to choose from ($this->is_validating()
302      * return false) you should probably have an maximum number of users you will
303      * return, and if more users than this match your search, you should instead
304      * return a message generated by the too_many_results() method. However, you
305      * should not do this when validating.
306      *
307      * If you are writing a new user_selector subclass, I strongly recommend you
308      * look at some of the subclasses later in this file and in admin/roles/lib.php.
309      * They should help you see exactly what you have to do.
310      *
311      * @param string $search the search string.
312      * @return array An array of arrays of users. The array keys of the outer
313      *      array should be the string names of optgroups. The keys of the inner
314      *      arrays should be userids, and the values should be user objects
315      *      containing at least the list of fields returned by the method
316      *      required_fields_sql(). If a user object has a ->disabled property
317      *      that is true, then that option will be displayed greyed out, and
318      *      will not be returned by get_selected_users.
319      */
320     public abstract function find_users($search);
322     /**
323      *
324      * Note: this function must be implemented if you use the search ajax field
325      *       (e.g. set $options['file'] = '/admin/filecontainingyourclass.php';)
326      * @return array the options needed to recreate this user_selector.
327      */
328     protected function get_options() {
329         return array(
330             'class' => get_class($this),
331             'name' => $this->name,
332             'exclude' => $this->exclude,
333             'extrafields' => $this->extrafields,
334             'multiselect' => $this->multiselect
335         );
336     }
338     // Inner workings ==========================================================
340     /**
341      * @return boolean if true, we are validating a list of selected users,
342      *      rather than preparing a list of uesrs to choose from.
343      */
344     protected function is_validating() {
345         return !is_null($this->validatinguserids);
346     }
348     /**
349      * Get the list of users that were selected by doing optional_param then
350      * validating the result.
351      *
352      * @return array of user objects.
353      */
354     protected function load_selected_users() {
355         // See if we got anything.
356         $userids = optional_param($this->name, array(), PARAM_INTEGER);
357         if (empty($userids)) {
358             return array();
359         }
360         if (!$this->multiselect) {
361             $userids = array($userids);
362         }
364         // If we did, use the find_users method to validate the ids.
365         $this->validatinguserids = $userids;
366         $groupedusers = $this->find_users('');
367         $this->validatinguserids = null;
369         // Aggregate the resulting list back into a single one.
370         $users = array();
371         foreach ($groupedusers as $group) {
372             foreach ($group as $user) {
373                 if (!isset($users[$user->id]) && empty($user->disabled) && in_array($user->id, $userids)) {
374                     $users[$user->id] = $user;
375                 }
376             }
377         }
379         // If we are only supposed to be selecting a single user, make sure we do.
380         if (!$this->multiselect && count($users) > 1) {
381             $users = array_slice($users, 0, 1);
382         }
384         return $users;
385     }
387     /**
388      * @param string $u the table alias for the user table in the query being
389      *      built. May be ''.
390      * @return string fragment of SQL to go in the select list of the query.
391      */
392     protected function required_fields_sql($u) {
393         // Raw list of fields.
394         $fields = array('id', 'firstname', 'lastname');
395         $fields = array_merge($fields, $this->extrafields);
397         // Prepend the table alias.
398         if ($u) {
399             foreach ($fields as &$field) {
400                 $field = $u . '.' . $field;
401             }
402         }
403         return implode(',', $fields);
404     }
406     /**
407      * @param string $search the text to search for.
408      * @param string $u the table alias for the user table in the query being
409      *      built. May be ''.
410      * @return array an array with two elements, a fragment of SQL to go in the
411      *      where clause the query, and an array containing any required parameters.
412      *      this uses ? style placeholders.
413      */
414     protected function search_sql($search, $u) {
415         global $DB;
416         $params = array();
417         $tests = array();
419         if ($u) {
420             $u .= '.';
421         }
423         // If we have a $search string, put a field LIKE '$search%' condition on each field.
424         if ($search) {
425             $conditions = array(
426                 $DB->sql_fullname($u . 'firstname', $u . 'lastname'),
427                 $conditions[] = $u . 'lastname'
428             );
429             foreach ($this->extrafields as $field) {
430                 $conditions[] = $u . $field;
431             }
432             $ilike = ' ' . $DB->sql_ilike();
433             if ($this->searchanywhere) {
434                 $searchparam = '%' . $search . '%';
435             } else {
436                 $searchparam = $search . '%';
437             }
438             $i = 0;
439             foreach ($conditions as &$condition) {
440                 $condition .= "$ilike :con{$i}00";
441                 $params["con{$i}00"] = $searchparam;
442                 $i++;
443             }
444             $tests[] = '(' . implode(' OR ', $conditions) . ')';
445         }
447         // Add some additional sensible conditions
448         $tests[] = $u . "username <> 'guest'";
449         $tests[] = $u . 'deleted = 0';
450         $tests[] = $u . 'confirmed = 1';
452         // If we are being asked to exclude any users, do that.
453         if (!empty($this->exclude)) {
454             list($usertest, $userparams) = $DB->get_in_or_equal($this->exclude, SQL_PARAMS_NAMED, 'ex000', false);
455             $tests[] = $u . 'id ' . $usertest;
456             $params = array_merge($params, $userparams);
457         }
459         // If we are validating a set list of userids, add an id IN (...) test.
460         if (!empty($this->validatinguserids)) {
461             list($usertest, $userparams) = $DB->get_in_or_equal($this->validatinguserids, SQL_PARAMS_NAMED, 'val000');
462             $tests[] = $u . 'id ' . $usertest;
463             $params = array_merge($params, $userparams);
464         }
466         if (empty($tests)) {
467             $tests[] = '1 = 1';
468         }
470         // Combing the conditions and return.
471         return array(implode(' AND ', $tests), $params);
472     }
474     /**
475      * Used to generate a nice message when there are too many users to show.
476      * The message includes the number of users that currently match, and the
477      * text of the message depends on whether the search term is non-blank.
478      *
479      * @param string $search the search term, as passed in to the find users method.
480      * @param unknown_type $count the number of users that currently match.
481      * @return array in the right format to return from the find_users method.
482      */
483     protected function too_many_results($search, $count) {
484         if ($search) {
485             $a = new stdClass;
486             $a->count = $count;
487             $a->search = $search;
488             return array(get_string('toomanyusersmatchsearch', '', $a) => array(),
489                     get_string('pleasesearchmore') => array());
490         } else {
491             return array(get_string('toomanyuserstoshow', '', $count) => array(),
492                     get_string('pleaseusesearch') => array());
493         }
494     }
496     /**
497      * Output the list of <optgroup>s and <options>s that go inside the select.
498      * This method should do the same as the JavaScript method
499      * user_selector.prototype.handle_response.
500      *
501      * @param array $groupedusers an array, as returned by find_users.
502      * @return string HTML code.
503      */
504     protected function output_options($groupedusers, $search) {
505         $output = '';
507         // Ensure that the list of previously selected users is up to date.
508         $this->get_selected_users();
510         // If $groupedusers is empty, make a 'no matching users' group. If there is
511         // only one selected user, set a flag to select them if that option is turned on.
512         $select = false;
513         if (empty($groupedusers)) {
514             if (!empty($search)) {
515                 $groupedusers = array(get_string('nomatchingusers', '', $search) => array());
516             } else {
517                 $groupedusers = array(get_string('none') => array());
518             }
519         } else if ($this->autoselectunique && count($groupedusers) == 1 &&
520                 count(reset($groupedusers)) == 1) {
521             $select = true;
522             if (!$this->multiselect) {
523                 $this->selected = array();
524             }
525         }
527         // Output each optgroup.
528         foreach ($groupedusers as $groupname => $users) {
529             $output .= $this->output_optgroup($groupname, $users, $select);
530         }
532         // If there were previously selected users who do not match the search, show them too.
533         if ($this->preserveselected && !empty($this->selected)) {
534             $output .= $this->output_optgroup(get_string('previouslyselectedusers', '', $search), $this->selected, true);
535         }
537         // This method trashes $this->selected, so clear the cache so it is
538         // rebuilt before anyone tried to use it again.
539         $this->selected = null;
541         return $output;
542     }
544     /**
545      * Output one particular optgroup. Used by the preceding function output_options.
546      *
547      * @param string $groupname the label for this optgroup.
548      * @param array $users the users to put in this optgroup.
549      * @param boolean $select if true, select the users in this group.
550      * @return string HTML code.
551      */
552     protected function output_optgroup($groupname, $users, $select) {
553         if (!empty($users)) {
554             $output = '  <optgroup label="' . htmlspecialchars($groupname) . ' (' . count($users) . ')">' . "\n";
555             foreach ($users as $user) {
556                 $attributes = '';
557                 if (!empty($user->disabled)) {
558                     $attributes .= ' disabled="disabled"';
559                 } else if ($select || isset($this->selected[$user->id])) {
560                     $attributes .= ' selected="selected"';
561                 }
562                 unset($this->selected[$user->id]);
563                 $output .= '    <option' . $attributes . ' value="' . $user->id . '">' .
564                         $this->output_user($user) . "</option>\n";
565             }
566         } else {
567             $output = '  <optgroup label="' . htmlspecialchars($groupname) . '">' . "\n";
568             $output .= '    <option disabled="disabled">&nbsp;</option>' . "\n";
569         }
570         $output .= "  </optgroup>\n";
571         return $output;
572     }
574     /**
575      * Convert a user object to a string suitable for displaying as an option in the list box.
576      *
577      * @param object $user the user to display.
578      * @return string a string representation of the user.
579      */
580     public function output_user($user) {
581         $bits = array(
582             fullname($user)
583         );
584         foreach ($this->extrafields as $field) {
585             $bits[] = $user->$field;
586         }
587         return implode(', ', $bits);
588     }
590     /**
591      * @return string the caption for the search button.
592      */
593     protected function search_button_caption() {
594         return get_string('search');
595     }
597     // Initialise one of the option checkboxes, either from
598     // the request, or failing that from the user_preferences table, or
599     // finally from the given default.
600     private function initialise_option($name, $default) {
601         $param = optional_param($name, null, PARAM_BOOL);
602         if (is_null($param)) {
603             return get_user_preferences($name, $default);
604         } else {
605             set_user_preference($name, $param);
606             return $param;
607         }
608     }
610     // Output one of the options checkboxes.
611     private function option_checkbox($name, $on, $label) {
612         if ($on) {
613             $checked = ' checked="checked"';
614         } else {
615             $checked = '';
616         }
617         $name = 'userselector_' . $name;
618         $output = '<p><input type="hidden" name="' . $name . '" value="0" />' .
619                 // For the benefit of brain-dead IE, the id must be different from the name of the hidden form field above.
620                 // It seems that document.getElementById('frog') in IE will return and element with name="frog".
621                 '<input type="checkbox" id="' . $name . 'id" name="' . $name . '" value="1"' . $checked . ' /> ' .
622                 '<label for="' . $name . 'id">' . $label . "</label></p>\n";
623         user_preference_allow_ajax_update($name, PARAM_BOOL);
624         return $output;
625     }
627     /**
628      * @param boolean $optiontracker if true, initialise JavaScript for updating the user prefs.
629      * @return any HTML needed here.
630      */
631     protected function initialise_javascript($search) {
632         global $USER, $PAGE, $OUTPUT;
633         $output = '';
635         // Put the options into the session, to allow search.php to respond to the ajax requests.
636         $options = $this->get_options();
637         $hash = md5(serialize($options));
638         $USER->userselectors[$hash] = $options;
640         // Initialise the selector.
641         $PAGE->requires->js_init_call('M.core_user.init_user_selector', array($this->name, $hash, $this->extrafields, $search), false, self::$jsmodule);
642         return $output;
643     }
646 // User selectors for managing group members ==================================
648 /**
649  * Base class to avoid duplicating code.
650  */
651 abstract class groups_user_selector_base extends user_selector_base {
652     protected $groupid;
653     protected $courseid;
655     /**
656      * @param string $name control name
657      * @param array $options should have two elements with keys groupid and courseid.
658      */
659     public function __construct($name, $options) {
660         global $CFG;
661         parent::__construct($name, $options);
662         $this->groupid = $options['groupid'];
663         $this->courseid = $options['courseid'];
664         require_once($CFG->dirroot . '/group/lib.php');
665     }
667     protected function get_options() {
668         $options = parent::get_options();
669         $options['groupid'] = $this->groupid;
670         $options['courseid'] = $this->courseid;
671         return $options;
672     }
674     /**
675      * @param array $roles array in the format returned by groups_calculate_role_people.
676      * @return array array in the format find_users is supposed to return.
677      */
678     protected function convert_array_format($roles, $search) {
679         if (empty($roles)) {
680             $roles = array();
681         }
682         $groupedusers = array();
683         foreach ($roles as $role) {
684             if ($search) {
685                 $a = new stdClass;
686                 $a->role = $role->name;
687                 $a->search = $search;
688                 $groupname = get_string('matchingsearchandrole', '', $a);
689             } else {
690                 $groupname = $role->name;
691             }
692             $groupedusers[$groupname] = $role->users;
693             foreach ($groupedusers[$groupname] as &$user) {
694                 unset($user->roles);
695                 $user->fullname = fullname($user);
696             }
697         }
698         return $groupedusers;
699     }
702 /**
703  * User selector subclass for the list of users who are in a certain group.
704  * Used on the add group memebers page.
705  */
706 class group_members_selector extends groups_user_selector_base {
707     public function find_users($search) {
708         list($wherecondition, $params) = $this->search_sql($search, 'u');
709         $roles = groups_get_members_by_role($this->groupid, $this->courseid,
710                 $this->required_fields_sql('u'), 'u.lastname, u.firstname',
711                 $wherecondition, $params);
712         return $this->convert_array_format($roles, $search);
713     }
716 /**
717  * User selector subclass for the list of users who are not in a certain group.
718  * Used on the add group members page.
719  */
720 class group_non_members_selector extends groups_user_selector_base {
721     const MAX_USERS_PER_PAGE = 100;
723     /**
724      * An array of user ids populated by find_users() used in print_user_summaries()
725      */
726     private $potentialmembersids = array();
728     public function output_user($user) {
729         return parent::output_user($user) . ' (' . $user->numgroups . ')';
730     }
732     /**
733      * Returns the user selector JavaScript module
734      * @return array
735      */
736     public function get_js_module() {
737         return self::$jsmodule;
738     }
740     /**
741      * Outputs a Javascript array containing the other groups non-members are in.
742      * Used on the add group members page.
743      */
744     public function print_user_summaries($courseid) {
745         global $DB;
747         echo <<<END
748         <script type="text/javascript">
749 //<![CDATA[
750 var userSummaries = Array(
751 END;
752         // Get other groups user already belongs to
753         $usergroups = array();
754         $potentialmembersids = $this->potentialmembersids;
755         if( empty($potentialmembersids)==false ) {
756             list($membersidsclause, $params) = $DB->get_in_or_equal($potentialmembersids, SQL_PARAMS_NAMED, 'pm0');
757             $sql = "SELECT u.id AS userid, g.*
758                    FROM {user} u
759                    JOIN {groups_members} gm ON u.id = gm.userid
760                    JOIN {groups} g ON gm.groupid = g.id
761                   WHERE u.id $membersidsclause AND g.courseid = :courseid ";
762             $params['courseid'] = $courseid;
763             if ($rs = $DB->get_recordset_sql($sql, $params)) {
764                 foreach ($rs as $usergroup) {
765                     $usergroups[$usergroup->userid][$usergroup->id] = $usergroup;
766                 }
767                 $rs->close();
768             }
770             $membercnt = count($potentialmembersids);
771             $i=1;
772             foreach ($potentialmembersids as $userid) {
773              if (isset($usergroups[$userid])) {
774                  $usergrouplist = '<ul>';
776                  foreach ($usergroups[$userid] as $groupitem) {
777                      $usergrouplist .= '<li>'.addslashes_js(format_string($groupitem->name)).'</li>';
778                  }
779                  $usergrouplist .= '</ul>';
780              }
781              else {
782                  $usergrouplist = '';
783              }
784              echo "'$usergrouplist'";
785              if ($i < $membercnt) {
786                  echo ', ';
787              }
788              $i++;
789             }
790         }
791 echo <<<END
792 );
793 //]]>
794 </script>
795 END;
796     }
798     public function find_users($search) {
799         global $DB;
801         // Get list of allowed roles.
802         $context = get_context_instance(CONTEXT_COURSE, $this->courseid);
803         if (!$validroleids = groups_get_possible_roles($context)) {
804             return array();
805         }
806         list($roleids, $roleparams) = $DB->get_in_or_equal($validroleids, SQL_PARAMS_NAMED, 'r00');
808         // Get the search condition.
809         list($searchcondition, $searchparams) = $this->search_sql($search, 'u');
811         // Build the SQL
812         list($enrolsql, $enrolparams) = get_enrolled_sql($context);
813         $fields = "SELECT r.id AS roleid, r.shortname AS roleshortname, r.name AS rolename, u.id AS userid,
814                           " . $this->required_fields_sql('u') . ",
815                           (SELECT count(igm.groupid)
816                              FROM {groups_members} igm
817                              JOIN {groups} ig ON igm.groupid = ig.id
818                             WHERE igm.userid = u.id AND ig.courseid = :courseid) AS numgroups";
819         $sql = "   FROM {user} u
820                    JOIN ($enrolsql) e ON e.id = u.id
821                    JOIN {role_assignments} ra ON ra.userid = u.id
822                    JOIN {role} r ON r.id = ra.roleid
823                   WHERE ra.contextid " . get_related_contexts_string($context) . "
824                         AND u.deleted = 0
825                         AND ra.roleid $roleids
826                         AND u.id NOT IN (SELECT userid
827                                           FROM {groups_members}
828                                          WHERE groupid = :groupid)
829                         AND $searchcondition";
830         $orderby = "ORDER BY u.lastname, u.firstname";
832         $params = array_merge($searchparams, $roleparams, $enrolparams);
833         $params['courseid'] = $this->courseid;
834         $params['groupid']  = $this->groupid;
836         if (!$this->is_validating()) {
837             $potentialmemberscount = $DB->count_records_sql("SELECT COUNT(DISTINCT u.id) $sql", $params);
838             if ($potentialmemberscount > group_non_members_selector::MAX_USERS_PER_PAGE) {
839                 return $this->too_many_results($search, $potentialmemberscount);
840             }
841         }
843         $rs = $DB->get_recordset_sql("$fields $sql $orderby", $params);
844         $roles =  groups_calculate_role_people($rs, $context);
846         //don't hold onto user IDs if we're doing validation
847         if (empty($this->validatinguserids) ) {
848             if($roles) {
849                 foreach($roles as $k=>$v) {
850                     if($v) {
851                         foreach($v->users as $uid=>$userobject) {
852                             $this->potentialmembersids[] = $uid;
853                         }
854                     }
855                 }
856             }
857         }
859         return $this->convert_array_format($roles, $search);
860     }