lib.php 35.2 KB
Newer Older
1
<?php
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 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 <http://www.gnu.org/licenses/>.
16
17
18
19

/**
 * Code for ajax user selectors.
 *
20
 * @package   core_user
21
22
 * @copyright 1999 onwards Martin Dougiamas  http://dougiamas.com
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
24
25
26
27
28
29
30
31
 */

/**
 * The default size of a user selector.
 */
define('USER_SELECTOR_DEFAULT_ROWS', 20);

/**
 * Base class for user selectors.
32
33
34
35
 *
 * In your theme, you must give each user-selector a defined width. If the
 * user selector has name="myid", then the div myid_wrapper must have a width
 * specified.
36
37
38
 *
 * @copyright 1999 onwards Martin Dougiamas  http://dougiamas.com
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
39
40
 */
abstract class user_selector_base {
41
    /** @var string The control name (and id) in the HTML. */
42
    protected $name;
43
    /** @var array Extra fields to search on and return in addition to firstname and lastname. */
44
    protected $extrafields;
45
46
47
    /** @var object Context used for capability checks regarding this selector (does
     * not necessarily restrict user list) */
    protected $accesscontext;
48
    /** @var boolean Whether the conrol should allow selection of many users, or just one. */
49
    protected $multiselect = true;
50
    /** @var int The height this control should have, in rows. */
51
    protected $rows = USER_SELECTOR_DEFAULT_ROWS;
52
    /** @var array A list of userids that should not be returned by this control. */
53
    protected $exclude = array();
54
    /** @var array|null A list of the users who are selected. */
55
    protected $selected = null;
56
    /** @var boolean When the search changes, do we keep previously selected options that do
57
58
     * not match the new search term? */
    protected $preserveselected = false;
59
    /** @var boolean If only one user matches the search, should we select them automatically. */
60
    protected $autoselectunique = false;
61
    /** @var boolean When searching, do we only match the starts of fields (better performance)
62
63
     * or do we match occurrences anywhere? */
    protected $searchanywhere = false;
64
    /** @var mixed This is used by get selected users */
65
    protected $validatinguserids = null;
66

67
68
    /**  @var boolean Used to ensure we only output the search options for one user selector on
     * each page. */
69
    private static $searchoptionsoutput = false;
70

71
72
73
74
    /** @var array JavaScript YUI3 Module definition */
    protected static $jsmodule = array(
                'name' => 'user_selector',
                'fullpath' => '/user/selector/module.js',
75
                'requires'  => array('node', 'event-custom', 'datasource', 'json', 'moodle-core-notification'),
76
77
78
79
80
                'strings' => array(
                    array('previouslyselectedusers', 'moodle', '%%SEARCHTERM%%'),
                    array('nomatchingusers', 'moodle', '%%SEARCHTERM%%'),
                    array('none', 'moodle')
                ));
81

82
83
    /** @var int this is used to define maximum number of users visible in list */
    public $maxusersperpage = 100;
84

85
86
87
    /** @var boolean Whether to override fullname() */
    public $viewfullnames = false;

88
    /**
89
     * Constructor. Each subclass must have a constructor with this signature.
90
91
     *
     * @param string $name the control name/id for use in the HTML.
92
93
     * @param array $options other options needed to construct this selector.
     * You must be able to clone a userselector by doing new get_class($us)($us->get_name(), $us->get_options());
94
     */
95
    public function __construct($name, $options = array()) {
96
        global $CFG, $PAGE;
97
98

        // Initialise member variables from constructor arguments.
99
        $this->name = $name;
100

101
        // Use specified context for permission checks, system context if not specified.
102
103
104
        if (isset($options['accesscontext'])) {
            $this->accesscontext = $options['accesscontext'];
        } else {
105
            $this->accesscontext = context_system::instance();
106
107
        }

108
109
        if (isset($options['extrafields'])) {
            $this->extrafields = $options['extrafields'];
110
111
112
        } else if (!empty($CFG->showuseridentity) &&
                has_capability('moodle/site:viewuseridentity', $this->accesscontext)) {
            $this->extrafields = explode(',', $CFG->showuseridentity);
113
114
        } else {
            $this->extrafields = array();
115
        }
116
117
118
        if (isset($options['exclude']) && is_array($options['exclude'])) {
            $this->exclude = $options['exclude'];
        }
119
120
121
        if (isset($options['multiselect'])) {
            $this->multiselect = $options['multiselect'];
        }
122
123

        // Read the user prefs / optional_params that we use.
124
125
126
        $this->preserveselected = $this->initialise_option('userselector_preserveselected', $this->preserveselected);
        $this->autoselectunique = $this->initialise_option('userselector_autoselectunique', $this->autoselectunique);
        $this->searchanywhere = $this->initialise_option('userselector_searchanywhere', $this->searchanywhere);
127
128
129
130

        if (!empty($CFG->maxusersperpage)) {
            $this->maxusersperpage = $CFG->maxusersperpage;
        }
131
132
133
    }

    /**
134
135
136
     * All to the list of user ids that this control will not select.
     *
     * For example, on the role assign page, we do not list the users who already have the role in question.
137
138
139
140
141
142
143
144
145
146
147
     *
     * @param array $arrayofuserids the user ids to exclude.
     */
    public function exclude($arrayofuserids) {
        $this->exclude = array_unique(array_merge($this->exclude, $arrayofuserids));
    }

    /**
     * Clear the list of excluded user ids.
     */
    public function clear_exclusions() {
148
        $this->exclude = array();
149
150
151
    }

    /**
152
153
     * Returns the list of user ids that this control will not select.
     *
154
155
156
157
158
159
160
     * @return array the list of user ids that this control will not select.
     */
    public function get_exclusions() {
        return clone($this->exclude);
    }

    /**
161
162
163
     * The users that were selected.
     *
     * This is a more sophisticated version of optional_param($this->name, array(), PARAM_INT) that validates the
164
     * returned list of ids against the rules for this user selector.
165
166
     *
     * @return array of user objects.
167
168
169
170
171
172
173
174
175
     */
    public function get_selected_users() {
        // Do a lazy load.
        if (is_null($this->selected)) {
            $this->selected = $this->load_selected_users();
        }
        return $this->selected;
    }

176
177
    /**
     * Convenience method for when multiselect is false (throws an exception if not).
178
179
     *
     * @throws moodle_exception
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
     * @return object the selected user object, or null if none.
     */
    public function get_selected_user() {
        if ($this->multiselect) {
            throw new moodle_exception('cannotcallusgetselecteduser');
        }
        $users = $this->get_selected_users();
        if (count($users) == 1) {
            return reset($users);
        } else if (count($users) == 0) {
            return null;
        } else {
            throw new moodle_exception('userselectortoomany');
        }
    }

196
    /**
197
198
     * Invalidates the list of selected users.
     *
199
200
201
202
203
204
205
206
207
     * If you update the database in such a way that it is likely to change the
     * list of users that this component is allowed to select from, then you
     * must call this method. For example, on the role assign page, after you have
     * assigned some roles to some users, you should call this.
     */
    public function invalidate_selected_users() {
        $this->selected = null;
    }

208
209
    /**
     * Output this user_selector as HTML.
210
     *
211
212
213
214
     * @param boolean $return if true, return the HTML as a string instead of outputting it.
     * @return mixed if $return is true, returns the HTML as a string, otherwise returns nothing.
     */
    public function display($return = false) {
215
216
        global $PAGE;

217
        // Get the list of requested users.
218
        $search = optional_param($this->name . '_searchtext', '', PARAM_RAW);
219
220
221
        if (optional_param($this->name . '_clearbutton', false, PARAM_BOOL)) {
            $search = '';
        }
222
223
224
225
226
227
228
229
230
231
232
        $groupedusers = $this->find_users($search);

        // Output the select.
        $name = $this->name;
        $multiselect = '';
        if ($this->multiselect) {
            $name .= '[]';
            $multiselect = 'multiple="multiple" ';
        }
        $output = '<div class="userselector" id="' . $this->name . '_wrapper">' . "\n" .
                '<select name="' . $name . '" id="' . $this->name . '" ' .
233
                $multiselect . 'size="' . $this->rows . '" class="form-control no-overflow">' . "\n";
234
235

        // Populate the select.
236
        $output .= $this->output_options($groupedusers, $search);
237
238

        // Output the search controls.
239
        $output .= "</select>\n<div class=\"form-inline\">\n";
240
        $output .= '<input type="text" name="' . $this->name . '_searchtext" id="' .
241
                $this->name . '_searchtext" size="15" value="' . s($search) . '" class="form-control"/>';
242
        $output .= '<input type="submit" name="' . $this->name . '_searchbutton" id="' .
243
                $this->name . '_searchbutton" value="' . $this->search_button_caption() . '" class="btn btn-secondary"/>';
244
        $output .= '<input type="submit" name="' . $this->name . '_clearbutton" id="' .
245
                $this->name . '_clearbutton" value="' . get_string('clear') . '" class="btn btn-secondary"/>';
246
247
248
249
250

        // And the search options.
        $optionsoutput = false;
        if (!user_selector_base::$searchoptionsoutput) {
            $output .= print_collapsible_region_start('', 'userselector_options',
251
252
253
254
255
256
257
                get_string('searchoptions'), 'userselector_optionscollapsed', true, true);
            $output .= $this->option_checkbox('preserveselected', $this->preserveselected,
                get_string('userselectorpreserveselected'));
            $output .= $this->option_checkbox('autoselectunique', $this->autoselectunique,
                get_string('userselectorautoselectunique'));
            $output .= $this->option_checkbox('searchanywhere', $this->searchanywhere,
                get_string('userselectorsearchanywhere'));
258
            $output .= print_collapsible_region_end(true);
259

260
            $PAGE->requires->js_init_call('M.core_user.init_user_selector_options_tracker', array(), false, self::$jsmodule);
261
262
            user_selector_base::$searchoptionsoutput = true;
        }
263
264
        $output .= "</div>\n</div>\n\n";

265
        // Initialise the ajax functionality.
266
        $output .= $this->initialise_javascript($search);
267

268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
        // Return or output it.
        if ($return) {
            return $output;
        } else {
            echo $output;
        }
    }

    /**
     * The height this control will be displayed, in rows.
     *
     * @param integer $numrows the desired height.
     */
    public function set_rows($numrows) {
        $this->rows = $numrows;
    }

    /**
286
287
     * Returns the number of rows to display in this control.
     *
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
     * @return integer the height this control will be displayed, in rows.
     */
    public function get_rows() {
        return $this->rows;
    }

    /**
     * Whether this control will allow selection of many, or just one user.
     *
     * @param boolean $multiselect true = allow multiple selection.
     */
    public function set_multiselect($multiselect) {
        $this->multiselect = $multiselect;
    }

    /**
304
305
     * Returns true is multiselect should be allowed.
     *
306
307
308
309
310
311
312
     * @return boolean whether this control will allow selection of more than one user.
     */
    public function is_multiselect() {
        return $this->multiselect;
    }

    /**
313
314
     * Returns the id/name of this control.
     *
315
316
317
318
319
320
     * @return string the id/name that this control will have in the HTML.
     */
    public function get_name() {
        return $this->name;
    }

321
    /**
322
     * Set the user fields that are displayed in the selector in addition to the user's name.
323
324
325
326
327
328
329
     *
     * @param array $fields a list of field names that exist in the user table.
     */
    public function set_extra_fields($fields) {
        $this->extrafields = $fields;
    }

330
331
332
333
334
    /**
     * Search the database for users matching the $search string, and any other
     * conditions that apply. The SQL for testing whether a user matches the
     * search string should be obtained by calling the search_sql method.
     *
335
336
337
338
339
340
341
342
343
344
     * This method is used both when getting the list of choices to display to
     * the user, and also when validating a list of users that was selected.
     *
     * When preparing a list of users to choose from ($this->is_validating()
     * return false) you should probably have an maximum number of users you will
     * return, and if more users than this match your search, you should instead
     * return a message generated by the too_many_results() method. However, you
     * should not do this when validating.
     *
     * If you are writing a new user_selector subclass, I strongly recommend you
345
346
     * look at some of the subclasses later in this file and in admin/roles/lib.php.
     * They should help you see exactly what you have to do.
347
     *
348
349
350
351
352
     * @param string $search the search string.
     * @return array An array of arrays of users. The array keys of the outer
     *      array should be the string names of optgroups. The keys of the inner
     *      arrays should be userids, and the values should be user objects
     *      containing at least the list of fields returned by the method
353
354
355
     *      required_fields_sql(). If a user object has a ->disabled property
     *      that is true, then that option will be displayed greyed out, and
     *      will not be returned by get_selected_users.
356
     */
357
    public abstract function find_users($search);
358

359
    /**
360
     *
361
362
     * Note: this function must be implemented if you use the search ajax field
     *       (e.g. set $options['file'] = '/admin/filecontainingyourclass.php';)
363
364
     * @return array the options needed to recreate this user_selector.
     */
365
366
367
    protected function get_options() {
        return array(
            'class' => get_class($this),
368
            'name' => $this->name,
369
            'exclude' => $this->exclude,
370
            'extrafields' => $this->extrafields,
371
372
            'multiselect' => $this->multiselect,
            'accesscontext' => $this->accesscontext,
373
374
375
        );
    }

376
    /**
377
378
     * Returns true if this control is validating a list of users.
     *
379
380
381
382
383
384
385
     * @return boolean if true, we are validating a list of selected users,
     *      rather than preparing a list of uesrs to choose from.
     */
    protected function is_validating() {
        return !is_null($this->validatinguserids);
    }

386
    /**
387
     * Get the list of users that were selected by doing optional_param then validating the result.
388
389
390
     *
     * @return array of user objects.
     */
391
392
    protected function load_selected_users() {
        // See if we got anything.
393
        if ($this->multiselect) {
394
395
            $userids = optional_param_array($this->name, array(), PARAM_INT);
        } else if ($userid = optional_param($this->name, 0, PARAM_INT)) {
396
            $userids = array($userid);
397
        }
398
        // If there are no users there is nobody to load.
399
400
401
        if (empty($userids)) {
            return array();
        }
402
403
404
405
406
407
408
409
410

        // If we did, use the find_users method to validate the ids.
        $this->validatinguserids = $userids;
        $groupedusers = $this->find_users('');
        $this->validatinguserids = null;

        // Aggregate the resulting list back into a single one.
        $users = array();
        foreach ($groupedusers as $group) {
411
            foreach ($group as $user) {
412
                if (!isset($users[$user->id]) && empty($user->disabled) && in_array($user->id, $userids)) {
413
414
415
                    $users[$user->id] = $user;
                }
            }
416
        }
417
418
419
420
421
422

        // If we are only supposed to be selecting a single user, make sure we do.
        if (!$this->multiselect && count($users) > 1) {
            $users = array_slice($users, 0, 1);
        }

423
424
425
        return $users;
    }

426
    /**
427
428
     * Returns SQL to select required fields.
     *
429
430
431
432
     * @param string $u the table alias for the user table in the query being
     *      built. May be ''.
     * @return string fragment of SQL to go in the select list of the query.
     */
433
434
    protected function required_fields_sql($u) {
        // Raw list of fields.
435
        $fields = array('id');
436
        // Add additional name fields.
437
        $fields = array_merge($fields, get_all_user_name_fields(), $this->extrafields);
438
439
440
441
442
443
444
445
446
447

        // Prepend the table alias.
        if ($u) {
            foreach ($fields as &$field) {
                $field = $u . '.' . $field;
            }
        }
        return implode(',', $fields);
    }

448
    /**
449
450
     * Returns an array with SQL to perform a search and the params that go into it.
     *
451
452
453
454
455
456
457
     * @param string $search the text to search for.
     * @param string $u the table alias for the user table in the query being
     *      built. May be ''.
     * @return array an array with two elements, a fragment of SQL to go in the
     *      where clause the query, and an array containing any required parameters.
     *      this uses ? style placeholders.
     */
458
    protected function search_sql($search, $u) {
459
        return users_search_sql($search, $u, $this->searchanywhere, $this->extrafields,
460
                $this->exclude, $this->validatinguserids);
461
462
    }

463
464
    /**
     * Used to generate a nice message when there are too many users to show.
465
     *
466
467
468
469
     * The message includes the number of users that currently match, and the
     * text of the message depends on whether the search term is non-blank.
     *
     * @param string $search the search term, as passed in to the find users method.
470
     * @param int $count the number of users that currently match.
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
     * @return array in the right format to return from the find_users method.
     */
    protected function too_many_results($search, $count) {
        if ($search) {
            $a = new stdClass;
            $a->count = $count;
            $a->search = $search;
            return array(get_string('toomanyusersmatchsearch', '', $a) => array(),
                    get_string('pleasesearchmore') => array());
        } else {
            return array(get_string('toomanyuserstoshow', '', $count) => array(),
                    get_string('pleaseusesearch') => array());
        }
    }

486
487
    /**
     * Output the list of <optgroup>s and <options>s that go inside the select.
488
     *
489
490
491
     * This method should do the same as the JavaScript method
     * user_selector.prototype.handle_response.
     *
492
     * @param array $groupedusers an array, as returned by find_users.
493
     * @param string $search
494
     * @return string HTML code.
495
     */
496
    protected function output_options($groupedusers, $search) {
497
498
499
500
501
        $output = '';

        // Ensure that the list of previously selected users is up to date.
        $this->get_selected_users();

502
503
        // If $groupedusers is empty, make a 'no matching users' group. If there is
        // only one selected user, set a flag to select them if that option is turned on.
504
        $select = false;
505
        if (empty($groupedusers)) {
506
507
508
509
510
            if (!empty($search)) {
                $groupedusers = array(get_string('nomatchingusers', '', $search) => array());
            } else {
                $groupedusers = array(get_string('none') => array());
            }
511
512
        } else if ($this->autoselectunique && count($groupedusers) == 1 &&
                count(reset($groupedusers)) == 1) {
513
514
515
516
517
518
            $select = true;
            if (!$this->multiselect) {
                $this->selected = array();
            }
        }

519
520
521
522
523
524
        // 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.
525
        if ($this->preserveselected && !empty($this->selected)) {
526
            $output .= $this->output_optgroup(get_string('previouslyselectedusers', '', $search), $this->selected, true);
527
528
        }

529
        // This method trashes $this->selected, so clear the cache so it is rebuilt before anyone tried to use it again.
530
531
532
533
534
        $this->selected = null;

        return $output;
    }

535
536
537
538
539
540
541
542
    /**
     * Output one particular optgroup. Used by the preceding function output_options.
     *
     * @param string $groupname the label for this optgroup.
     * @param array $users the users to put in this optgroup.
     * @param boolean $select if true, select the users in this group.
     * @return string HTML code.
     */
543
544
    protected function output_optgroup($groupname, $users, $select) {
        if (!empty($users)) {
545
            $output = '  <optgroup label="' . htmlspecialchars($groupname) . ' (' . count($users) . ')">' . "\n";
546
            foreach ($users as $user) {
547
548
549
550
551
                $attributes = '';
                if (!empty($user->disabled)) {
                    $attributes .= ' disabled="disabled"';
                } else if ($select || isset($this->selected[$user->id])) {
                    $attributes .= ' selected="selected"';
552
553
                }
                unset($this->selected[$user->id]);
554
                $output .= '    <option' . $attributes . ' value="' . $user->id . '">' .
555
                        $this->output_user($user) . "</option>\n";
556
                if (!empty($user->infobelow)) {
557
                    // Poor man's indent  here is because CSS styles do not work in select options, except in Firefox.
558
559
560
                    $output .= '    <option disabled="disabled" class="userselector-infobelow">' .
                            '&nbsp;&nbsp;&nbsp;&nbsp;' . s($user->infobelow) . '</option>';
                }
561
562
            }
        } else {
563
            $output = '  <optgroup label="' . htmlspecialchars($groupname) . '">' . "\n";
564
            $output .= '    <option disabled="disabled">&nbsp;</option>' . "\n";
565
        }
566
        $output .= "  </optgroup>\n";
567
568
569
570
        return $output;
    }

    /**
571
     * Convert a user object to a string suitable for displaying as an option in the list box.
572
573
     *
     * @param object $user the user to display.
574
     * @return string a string representation of the user.
575
     */
576
    public function output_user($user) {
577
        $out = fullname($user, $this->viewfullnames);
578
579
580
581
582
583
        if ($this->extrafields) {
            $displayfields = array();
            foreach ($this->extrafields as $field) {
                $displayfields[] = $user->{$field};
            }
            $out .= ' (' . implode(', ', $displayfields) . ')';
584
        }
585
        return $out;
586
587
588
    }

    /**
589
590
     * Returns the string to use for the search button caption.
     *
591
592
593
594
595
     * @return string the caption for the search button.
     */
    protected function search_button_caption() {
        return get_string('search');
    }
596

597
598
599
600
601
602
603
604
    /**
     * Initialise one of the option checkboxes, either from  the request, or failing that from the
     * user_preferences table, or finally from the given default.
     *
     * @param string $name
     * @param mixed $default
     * @return mixed|null|string
     */
605
606
607
608
609
610
611
612
613
614
    private function initialise_option($name, $default) {
        $param = optional_param($name, null, PARAM_BOOL);
        if (is_null($param)) {
            return get_user_preferences($name, $default);
        } else {
            set_user_preference($name, $param);
            return $param;
        }
    }

615
616
617
618
619
620
621
622
    /**
     * Output one of the options checkboxes.
     *
     * @param string $name
     * @param string $on
     * @param string $label
     * @return string
     */
623
624
625
626
627
628
629
    private function option_checkbox($name, $on, $label) {
        if ($on) {
            $checked = ' checked="checked"';
        } else {
            $checked = '';
        }
        $name = 'userselector_' . $name;
630
631
632
633
634
635
636
637
        // For the benefit of brain-dead IE, the id must be different from the name of the hidden form field above.
        // It seems that document.getElementById('frog') in IE will return and element with name="frog".
        $output = '<div class="form-check"><input type="hidden" name="' . $name . '" value="0" />' .
                    '<label class="form-check-label" for="' . $name . 'id">' .
                        '<input class="form-check-input" type="checkbox" id="' . $name . 'id" name="' . $name .
                            '" value="1"' . $checked . ' /> ' . $label .
                    "</label>
                   </div>\n";
638
639
640
641
        user_preference_allow_ajax_update($name, PARAM_BOOL);
        return $output;
    }

642
    /**
643
644
645
646
     * Initialises JS for this control.
     *
     * @param string $search
     * @return string any HTML needed here.
647
     */
648
    protected function initialise_javascript($search) {
649
        global $USER, $PAGE, $OUTPUT;
650
651
652
653
654
655
656
657
        $output = '';

        // 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.
658
659
660
661
662
663
        $PAGE->requires->js_init_call(
            'M.core_user.init_user_selector',
            array($this->name, $hash, $this->extrafields, $search),
            false,
            self::$jsmodule
        );
664
665
666
667
        return $output;
    }
}

668
669
/**
 * Base class to avoid duplicating code.
670
671
672
 *
 * @copyright 1999 onwards Martin Dougiamas  http://dougiamas.com
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
673
 */
674
abstract class groups_user_selector_base extends user_selector_base {
675
    /** @var int */
676
    protected $groupid;
677
    /** @var  int */
678
679
680
    protected $courseid;

    /**
681
682
     * Constructor.
     *
683
684
685
686
687
     * @param string $name control name
     * @param array $options should have two elements with keys groupid and courseid.
     */
    public function __construct($name, $options) {
        global $CFG;
688
        $options['accesscontext'] = context_course::instance($options['courseid']);
689
690
691
692
693
694
        parent::__construct($name, $options);
        $this->groupid = $options['groupid'];
        $this->courseid = $options['courseid'];
        require_once($CFG->dirroot . '/group/lib.php');
    }

695
696
697
698
    /**
     * Returns options for this selector.
     * @return array
     */
699
700
701
702
703
704
705
706
    protected function get_options() {
        $options = parent::get_options();
        $options['groupid'] = $this->groupid;
        $options['courseid'] = $this->courseid;
        return $options;
    }

    /**
707
708
     * Creates an organised array from given data.
     *
709
     * @param array $roles array in the format returned by groups_calculate_role_people.
710
     * @param string $search
711
712
     * @return array array in the format find_users is supposed to return.
     */
713
    protected function convert_array_format($roles, $search) {
714
715
716
717
718
        if (empty($roles)) {
            $roles = array();
        }
        $groupedusers = array();
        foreach ($roles as $role) {
719
720
721
722
723
724
725
726
727
728
            if ($search) {
                $a = new stdClass;
                $a->role = $role->name;
                $a->search = $search;
                $groupname = get_string('matchingsearchandrole', '', $a);
            } else {
                $groupname = $role->name;
            }
            $groupedusers[$groupname] = $role->users;
            foreach ($groupedusers[$groupname] as &$user) {
729
730
                unset($user->roles);
                $user->fullname = fullname($user);
731
732
733
734
                if (!empty($user->component)) {
                    $user->infobelow = get_string('addedby', 'group',
                        get_string('pluginname', $user->component));
                }
735
736
737
738
739
740
741
742
            }
        }
        return $groupedusers;
    }
}

/**
 * User selector subclass for the list of users who are in a certain group.
743
 *
744
 * Used on the add group memebers page.
745
746
747
 *
 * @copyright 1999 onwards Martin Dougiamas  http://dougiamas.com
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
748
749
 */
class group_members_selector extends groups_user_selector_base {
750
751
752
753
754
755

    /**
     * Finds users to display in this control.
     * @param string $search
     * @return array
     */
756
    public function find_users($search) {
757
        list($wherecondition, $params) = $this->search_sql($search, 'u');
758
759
760

        list($sort, $sortparams) = users_order_by_sql('u', $search, $this->accesscontext);

761
        $roles = groups_get_members_by_role($this->groupid, $this->courseid,
762
                $this->required_fields_sql('u') . ', gm.component',
763
764
                $sort, $wherecondition, array_merge($params, $sortparams));

765
        return $this->convert_array_format($roles, $search);
766
767
768
769
770
    }
}

/**
 * User selector subclass for the list of users who are not in a certain group.
771
 *
772
 * Used on the add group members page.
773
774
775
 *
 * @copyright 1999 onwards Martin Dougiamas  http://dougiamas.com
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
776
777
 */
class group_non_members_selector extends groups_user_selector_base {
778
779
    /**
     * An array of user ids populated by find_users() used in print_user_summaries()
780
     * @var array
781
782
783
     */
    private $potentialmembersids = array();

784
785
786
787
788
789
    /**
     * Output user.
     *
     * @param stdClass $user
     * @return string
     */
790
    public function output_user($user) {
791
792
793
        return parent::output_user($user) . ' (' . $user->numgroups . ')';
    }

794
795
796
797
798
799
800
801
    /**
     * Returns the user selector JavaScript module
     * @return array
     */
    public function get_js_module() {
        return self::$jsmodule;
    }

802
    /**
803
804
805
806
807
808
809
     * Creates a global JS variable (userSummaries) that is used by the group selector
     * to print related information when the user clicks on a user in the groups UI.
     *
     * Used by /group/clientlib.js
     *
     * @global moodle_page $PAGE
     * @param int $courseid
810
811
     */
    public function print_user_summaries($courseid) {
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
        global $PAGE;
        $usersummaries = $this->get_user_summaries($courseid);
        $PAGE->requires->data_for_js('userSummaries', $usersummaries);
    }

    /**
     * Construct HTML lists of group-memberships of the current set of users.
     *
     * Used in user/selector/search.php to repopulate the userSummaries JS global
     * that is created in self::print_user_summaries() above.
     *
     * @param int $courseid The course
     * @return string[] Array of HTML lists of groups.
     */
    public function get_user_summaries($courseid) {
        global $DB;
828

829
        $usersummaries = array();
830

831
        // Get other groups user already belongs to.
832
833
        $usergroups = array();
        $potentialmembersids = $this->potentialmembersids;
834
        if (empty($potentialmembersids) == false) {
835
            list($membersidsclause, $params) = $DB->get_in_or_equal($potentialmembersids, SQL_PARAMS_NAMED, 'pm');
836
            $sql = "SELECT u.id AS userid, g.*
837
838
839
840
                    FROM {user} u
                    JOIN {groups_members} gm ON u.id = gm.userid
                    JOIN {groups} g ON gm.groupid = g.id
                    WHERE u.id $membersidsclause AND g.courseid = :courseid ";
841
            $params['courseid'] = $courseid;
842
843
844
            $rs = $DB->get_recordset_sql($sql, $params);
            foreach ($rs as $usergroup) {
                $usergroups[$usergroup->userid][$usergroup->id] = $usergroup;
845
            }
846
            $rs->close();
847
848

            foreach ($potentialmembersids as $userid) {
849
850
851
852
853
854
855
856
857
858
                if (isset($usergroups[$userid])) {
                    $usergrouplist = html_writer::start_tag('ul');
                    foreach ($usergroups[$userid] as $groupitem) {
                        $usergrouplist .= html_writer::tag('li', format_string($groupitem->name));
                    }
                    $usergrouplist .= html_writer::end_tag('ul');
                } else {
                    $usergrouplist = '';
                }
                $usersummaries[] = $usergrouplist;
859
860
            }
        }
861
        return $usersummaries;
862
863
    }

864
865
866
867
868
869
    /**
     * Finds users to display in this control.
     *
     * @param string $search
     * @return array
     */
870
871
872
873
    public function find_users($search) {
        global $DB;

        // Get list of allowed roles.
874
        $context = context_course::instance($this->courseid);
875
        if ($validroleids = groups_get_possible_roles($context)) {
876
            list($roleids, $roleparams) = $DB->get_in_or_equal($validroleids, SQL_PARAMS_NAMED, 'r');
877
878
879
        } else {
            $roleids = " = -1";
            $roleparams = array();
880
881
        }

882
        // We want to query both the current context and parent contexts.
883
884
        list($relatedctxsql, $relatedctxparams) =
            $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx');
885

886
887
888
        // Get the search condition.
        list($searchcondition, $searchparams) = $this->search_sql($search, 'u');

889
        // Build the SQL.
890
        list($enrolsql, $enrolparams) = get_enrolled_sql($context);
891
        $fields = "SELECT r.id AS roleid, u.id AS userid,
892
893
894
895
896
                          " . $this->required_fields_sql('u') . ",
                          (SELECT count(igm.groupid)
                             FROM {groups_members} igm
                             JOIN {groups} ig ON igm.groupid = ig.id
                            WHERE igm.userid = u.id AND ig.courseid = :courseid) AS numgroups";
897
        $sql = "   FROM {user} u
898
                   JOIN ($enrolsql) e ON e.id = u.id
899
              LEFT JOIN {role_assignments} ra ON (ra.userid = u.id AND ra.contextid $relatedctxsql AND ra.roleid $roleids)
900
              LEFT JOIN {role} r ON r.id = ra.roleid
901
              LEFT JOIN {groups_members} gm ON (gm.userid = u.id AND gm.groupid = :groupid)
902
                  WHERE u.deleted = 0
903
                        AND gm.id IS NULL
904
                        AND $searchcondition";
905
906
907

        list($sort, $sortparams) = users_order_by_sql('u', $search, $this->accesscontext);
        $orderby = ' ORDER BY ' . $sort;
908

909
        $params = array_merge($searchparams, $roleparams, $enrolparams, $relatedctxparams);
910
        $params['courseid'] = $this->courseid;
911
        $params['groupid']  = $this->groupid;
912

913
        if (!$this->is_validating()) {
914
            $potentialmemberscount = $DB->count_records_sql("SELECT COUNT(DISTINCT u.id) $sql", $params);
915
            if ($potentialmemberscount > $this->maxusersperpage) {
916
917
                return $this->too_many_results($search, $potentialmemberscount);
            }
918
919
        }

920
        $rs = $DB->get_recordset_sql("$fields $sql $orderby", array_merge($params, $sortparams));
921
        $roles = groups_calculate_role_people($rs, $context);
922

923
        // Don't hold onto user IDs if we're doing validation.
924
        if (empty($this->validatinguserids) ) {
925
926
927
928
            if ($roles) {
                foreach ($roles as $k => $v) {
                    if ($v) {
                        foreach ($v->users as $uid => $userobject) {
929
930
931
932
933
934
935
                            $this->potentialmembersids[] = $uid;
                        }
                    }
                }
            }
        }

936
        return $this->convert_array_format($roles, $search);
937
938
    }
}