See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 and 402] [Versions 401 and 403]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * Code for ajax user selectors. 19 * 20 * @package core_user 21 * @copyright 1999 onwards Martin Dougiamas http://dougiamas.com 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 25 /** 26 * The default size of a user selector. 27 */ 28 define('USER_SELECTOR_DEFAULT_ROWS', 20); 29 30 /** 31 * Base class for user selectors. 32 * 33 * In your theme, you must give each user-selector a defined width. If the 34 * user selector has name="myid", then the div myid_wrapper must have a width 35 * specified. 36 * 37 * @copyright 1999 onwards Martin Dougiamas http://dougiamas.com 38 * @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 /** @var object Context used for capability checks regarding this selector (does 46 * not necessarily restrict user list) */ 47 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 * not match the new search term? */ 58 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 * or do we match occurrences anywhere? */ 63 protected $searchanywhere = false; 64 /** @var mixed This is used by get selected users */ 65 protected $validatinguserids = null; 66 67 /** @var boolean Used to ensure we only output the search options for one user selector on 68 * each page. */ 69 private static $searchoptionsoutput = false; 70 71 /** @var array JavaScript YUI3 Module definition */ 72 protected static $jsmodule = array( 73 'name' => 'user_selector', 74 'fullpath' => '/user/selector/module.js', 75 'requires' => array('node', 'event-custom', 'datasource', 'json', 'moodle-core-notification'), 76 'strings' => array( 77 array('previouslyselectedusers', 'moodle', '%%SEARCHTERM%%'), 78 array('nomatchingusers', 'moodle', '%%SEARCHTERM%%'), 79 array('none', 'moodle') 80 )); 81 82 /** @var int this is used to define maximum number of users visible in list */ 83 public $maxusersperpage = 100; 84 85 /** @var boolean Whether to override fullname() */ 86 public $viewfullnames = false; 87 88 /** @var boolean Whether to include custom user profile fields */ 89 protected $includecustomfields = false; 90 /** @var string User fields selects for custom fields. */ 91 protected $userfieldsselects = ''; 92 /** @var string User fields join for custom fields. */ 93 protected $userfieldsjoin = ''; 94 /** @var array User fields params for custom fields. */ 95 protected $userfieldsparams = []; 96 /** @var array User fields mappings for custom fields. */ 97 protected $userfieldsmappings = []; 98 99 /** 100 * Constructor. Each subclass must have a constructor with this signature. 101 * 102 * @param string $name the control name/id for use in the HTML. 103 * @param array $options other options needed to construct this selector. 104 * You must be able to clone a userselector by doing new get_class($us)($us->get_name(), $us->get_options()); 105 */ 106 public function __construct($name, $options = array()) { 107 global $CFG, $PAGE; 108 109 // Initialise member variables from constructor arguments. 110 $this->name = $name; 111 112 // Use specified context for permission checks, system context if not specified. 113 if (isset($options['accesscontext'])) { 114 $this->accesscontext = $options['accesscontext']; 115 } else { 116 $this->accesscontext = context_system::instance(); 117 } 118 119 $this->viewfullnames = has_capability('moodle/site:viewfullnames', $this->accesscontext); 120 121 // Check if some legacy code tries to override $CFG->showuseridentity. 122 if (isset($options['extrafields'])) { 123 debugging('The user_selector classes do not support custom list of extra identity fields any more. '. 124 'Instead, the user identity fields defined by the site administrator will be used to respect '. 125 'the configured privacy setting.', DEBUG_DEVELOPER); 126 unset($options['extrafields']); 127 } 128 129 if (isset($options['includecustomfields'])) { 130 $this->includecustomfields = $options['includecustomfields']; 131 } else { 132 $this->includecustomfields = false; 133 } 134 135 // Populate the list of additional user identifiers to display. 136 if ($this->includecustomfields) { 137 $userfieldsapi = \core_user\fields::for_identity($this->accesscontext)->with_name(); 138 $this->extrafields = $userfieldsapi->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]); 139 [ 140 'selects' => $this->userfieldsselects, 141 'joins' => $this->userfieldsjoin, 142 'params' => $this->userfieldsparams, 143 'mappings' => $this->userfieldsmappings 144 ] = (array) $userfieldsapi->get_sql('u', true, '', '', false); 145 } else { 146 $this->extrafields = \core_user\fields::get_identity_fields($this->accesscontext, false); 147 } 148 149 if (isset($options['exclude']) && is_array($options['exclude'])) { 150 $this->exclude = $options['exclude']; 151 } 152 if (isset($options['multiselect'])) { 153 $this->multiselect = $options['multiselect']; 154 } 155 156 // Read the user prefs / optional_params that we use. 157 $this->preserveselected = $this->initialise_option('userselector_preserveselected', $this->preserveselected); 158 $this->autoselectunique = $this->initialise_option('userselector_autoselectunique', $this->autoselectunique); 159 $this->searchanywhere = $this->initialise_option('userselector_searchanywhere', $this->searchanywhere); 160 161 if (!empty($CFG->maxusersperpage)) { 162 $this->maxusersperpage = $CFG->maxusersperpage; 163 } 164 } 165 166 /** 167 * All to the list of user ids that this control will not select. 168 * 169 * For example, on the role assign page, we do not list the users who already have the role in question. 170 * 171 * @param array $arrayofuserids the user ids to exclude. 172 */ 173 public function exclude($arrayofuserids) { 174 $this->exclude = array_unique(array_merge($this->exclude, $arrayofuserids)); 175 } 176 177 /** 178 * Clear the list of excluded user ids. 179 */ 180 public function clear_exclusions() { 181 $this->exclude = array(); 182 } 183 184 /** 185 * Returns the list of user ids that this control will not select. 186 * 187 * @return array the list of user ids that this control will not select. 188 */ 189 public function get_exclusions() { 190 return clone($this->exclude); 191 } 192 193 /** 194 * The users that were selected. 195 * 196 * This is a more sophisticated version of optional_param($this->name, array(), PARAM_INT) that validates the 197 * returned list of ids against the rules for this user selector. 198 * 199 * @return array of user objects. 200 */ 201 public function get_selected_users() { 202 // Do a lazy load. 203 if (is_null($this->selected)) { 204 $this->selected = $this->load_selected_users(); 205 } 206 return $this->selected; 207 } 208 209 /** 210 * Convenience method for when multiselect is false (throws an exception if not). 211 * 212 * @throws moodle_exception 213 * @return object the selected user object, or null if none. 214 */ 215 public function get_selected_user() { 216 if ($this->multiselect) { 217 throw new moodle_exception('cannotcallusgetselecteduser'); 218 } 219 $users = $this->get_selected_users(); 220 if (count($users) == 1) { 221 return reset($users); 222 } else if (count($users) == 0) { 223 return null; 224 } else { 225 throw new moodle_exception('userselectortoomany'); 226 } 227 } 228 229 /** 230 * Invalidates the list of selected users. 231 * 232 * If you update the database in such a way that it is likely to change the 233 * list of users that this component is allowed to select from, then you 234 * must call this method. For example, on the role assign page, after you have 235 * assigned some roles to some users, you should call this. 236 */ 237 public function invalidate_selected_users() { 238 $this->selected = null; 239 } 240 241 /** 242 * Output this user_selector as HTML. 243 * 244 * @param boolean $return if true, return the HTML as a string instead of outputting it. 245 * @return mixed if $return is true, returns the HTML as a string, otherwise returns nothing. 246 */ 247 public function display($return = false) { 248 global $PAGE; 249 250 // Get the list of requested users. 251 $search = optional_param($this->name . '_searchtext', '', PARAM_RAW); 252 if (optional_param($this->name . '_clearbutton', false, PARAM_BOOL)) { 253 $search = ''; 254 } 255 $groupedusers = $this->find_users($search); 256 257 // Output the select. 258 $name = $this->name; 259 $multiselect = ''; 260 if ($this->multiselect) { 261 $name .= '[]'; 262 $multiselect = 'multiple="multiple" '; 263 } 264 $output = '<div class="userselector" id="' . $this->name . '_wrapper">' . "\n" . 265 '<select name="' . $name . '" id="' . $this->name . '" ' . 266 $multiselect . 'size="' . $this->rows . '" class="form-control no-overflow">' . "\n"; 267 268 // Populate the select. 269 $output .= $this->output_options($groupedusers, $search); 270 271 // Output the search controls. 272 $output .= "</select>\n<div class=\"form-inline\">\n"; 273 $output .= '<input type="text" name="' . $this->name . '_searchtext" id="' . 274 $this->name . '_searchtext" size="15" value="' . s($search) . '" class="form-control"/>'; 275 $output .= '<input type="submit" name="' . $this->name . '_searchbutton" id="' . 276 $this->name . '_searchbutton" value="' . $this->search_button_caption() . '" class="btn btn-secondary"/>'; 277 $output .= '<input type="submit" name="' . $this->name . '_clearbutton" id="' . 278 $this->name . '_clearbutton" value="' . get_string('clear') . '" class="btn btn-secondary"/>'; 279 280 // And the search options. 281 $optionsoutput = false; 282 if (!user_selector_base::$searchoptionsoutput) { 283 $output .= print_collapsible_region_start('', 'userselector_options', 284 get_string('searchoptions'), 'userselector_optionscollapsed', true, true); 285 $output .= $this->option_checkbox('preserveselected', $this->preserveselected, 286 get_string('userselectorpreserveselected')); 287 $output .= $this->option_checkbox('autoselectunique', $this->autoselectunique, 288 get_string('userselectorautoselectunique')); 289 $output .= $this->option_checkbox('searchanywhere', $this->searchanywhere, 290 get_string('userselectorsearchanywhere')); 291 $output .= print_collapsible_region_end(true); 292 293 $PAGE->requires->js_init_call('M.core_user.init_user_selector_options_tracker', array(), false, self::$jsmodule); 294 user_selector_base::$searchoptionsoutput = true; 295 } 296 $output .= "</div>\n</div>\n\n"; 297 298 // Initialise the ajax functionality. 299 $output .= $this->initialise_javascript($search); 300 301 // Return or output it. 302 if ($return) { 303 return $output; 304 } else { 305 echo $output; 306 } 307 } 308 309 /** 310 * The height this control will be displayed, in rows. 311 * 312 * @param integer $numrows the desired height. 313 */ 314 public function set_rows($numrows) { 315 $this->rows = $numrows; 316 } 317 318 /** 319 * Returns the number of rows to display in this control. 320 * 321 * @return integer the height this control will be displayed, in rows. 322 */ 323 public function get_rows() { 324 return $this->rows; 325 } 326 327 /** 328 * Whether this control will allow selection of many, or just one user. 329 * 330 * @param boolean $multiselect true = allow multiple selection. 331 */ 332 public function set_multiselect($multiselect) { 333 $this->multiselect = $multiselect; 334 } 335 336 /** 337 * Returns true is multiselect should be allowed. 338 * 339 * @return boolean whether this control will allow selection of more than one user. 340 */ 341 public function is_multiselect() { 342 return $this->multiselect; 343 } 344 345 /** 346 * Returns the id/name of this control. 347 * 348 * @return string the id/name that this control will have in the HTML. 349 */ 350 public function get_name() { 351 return $this->name; 352 } 353 354 /** 355 * Set the user fields that are displayed in the selector in addition to the user's name. 356 * 357 * @param array $fields a list of field names that exist in the user table. 358 */ 359 public function set_extra_fields($fields) { 360 debugging('The user_selector classes do not support custom list of extra identity fields any more. '. 361 'Instead, the user identity fields defined by the site administrator will be used to respect '. 362 'the configured privacy setting.', DEBUG_DEVELOPER); 363 } 364 365 /** 366 * Search the database for users matching the $search string, and any other 367 * conditions that apply. The SQL for testing whether a user matches the 368 * search string should be obtained by calling the search_sql method. 369 * 370 * This method is used both when getting the list of choices to display to 371 * the user, and also when validating a list of users that was selected. 372 * 373 * When preparing a list of users to choose from ($this->is_validating() 374 * return false) you should probably have an maximum number of users you will 375 * return, and if more users than this match your search, you should instead 376 * return a message generated by the too_many_results() method. However, you 377 * should not do this when validating. 378 * 379 * If you are writing a new user_selector subclass, I strongly recommend you 380 * look at some of the subclasses later in this file and in admin/roles/lib.php. 381 * They should help you see exactly what you have to do. 382 * 383 * @param string $search the search string. 384 * @return array An array of arrays of users. The array keys of the outer 385 * array should be the string names of optgroups. The keys of the inner 386 * arrays should be userids, and the values should be user objects 387 * containing at least the list of fields returned by the method 388 * required_fields_sql(). If a user object has a ->disabled property 389 * that is true, then that option will be displayed greyed out, and 390 * will not be returned by get_selected_users. 391 */ 392 public abstract function find_users($search); 393 394 /** 395 * 396 * Note: this function must be implemented if you use the search ajax field 397 * (e.g. set $options['file'] = '/admin/filecontainingyourclass.php';) 398 * @return array the options needed to recreate this user_selector. 399 */ 400 protected function get_options() { 401 return array( 402 'class' => get_class($this), 403 'name' => $this->name, 404 'exclude' => $this->exclude, 405 'multiselect' => $this->multiselect, 406 'accesscontext' => $this->accesscontext, 407 ); 408 } 409 410 /** 411 * Returns true if this control is validating a list of users. 412 * 413 * @return boolean if true, we are validating a list of selected users, 414 * rather than preparing a list of uesrs to choose from. 415 */ 416 protected function is_validating() { 417 return !is_null($this->validatinguserids); 418 } 419 420 /** 421 * Get the list of users that were selected by doing optional_param then validating the result. 422 * 423 * @return array of user objects. 424 */ 425 protected function load_selected_users() { 426 // See if we got anything. 427 if ($this->multiselect) { 428 $userids = optional_param_array($this->name, array(), PARAM_INT); 429 } else if ($userid = optional_param($this->name, 0, PARAM_INT)) { 430 $userids = array($userid); 431 } 432 // If there are no users there is nobody to load. 433 if (empty($userids)) { 434 return array(); 435 } 436 437 // If we did, use the find_users method to validate the ids. 438 $this->validatinguserids = $userids; 439 $groupedusers = $this->find_users(''); 440 $this->validatinguserids = null; 441 442 // Aggregate the resulting list back into a single one. 443 $users = array(); 444 foreach ($groupedusers as $group) { 445 foreach ($group as $user) { 446 if (!isset($users[$user->id]) && empty($user->disabled) && in_array($user->id, $userids)) { 447 $users[$user->id] = $user; 448 } 449 } 450 } 451 452 // If we are only supposed to be selecting a single user, make sure we do. 453 if (!$this->multiselect && count($users) > 1) { 454 $users = array_slice($users, 0, 1); 455 } 456 457 return $users; 458 } 459 460 /** 461 * Returns SQL to select required fields. 462 * 463 * @param string $u the table alias for the user table in the query being 464 * built. May be ''. 465 * @return string fragment of SQL to go in the select list of the query. 466 * @throws coding_exception if used when includecustomfields is true 467 */ 468 protected function required_fields_sql(string $u) { 469 if ($this->includecustomfields) { 470 throw new coding_exception('required_fields_sql() is not needed when includecustomfields is true, '. 471 'use $userfieldsselects instead.'); 472 } 473 474 // Raw list of fields. 475 $fields = array('id'); 476 // Add additional name fields. 477 $fields = array_merge($fields, \core_user\fields::get_name_fields(), $this->extrafields); 478 479 // Prepend the table alias. 480 if ($u) { 481 foreach ($fields as &$field) { 482 $field = $u . '.' . $field; 483 } 484 } 485 return implode(',', $fields); 486 } 487 488 /** 489 * Returns an array with SQL to perform a search and the params that go into it. 490 * 491 * @param string $search the text to search for. 492 * @param string $u the table alias for the user table in the query being 493 * built. May be ''. 494 * @return array an array with two elements, a fragment of SQL to go in the 495 * where clause the query, and an array containing any required parameters. 496 * this uses ? style placeholders. 497 */ 498 protected function search_sql(string $search, string $u): array { 499 $extrafields = $this->includecustomfields 500 ? array_values($this->userfieldsmappings) 501 : $this->extrafields; 502 503 return users_search_sql($search, $u, $this->searchanywhere, $extrafields, 504 $this->exclude, $this->validatinguserids); 505 } 506 507 /** 508 * Used to generate a nice message when there are too many users to show. 509 * 510 * The message includes the number of users that currently match, and the 511 * text of the message depends on whether the search term is non-blank. 512 * 513 * @param string $search the search term, as passed in to the find users method. 514 * @param int $count the number of users that currently match. 515 * @return array in the right format to return from the find_users method. 516 */ 517 protected function too_many_results($search, $count) { 518 if ($search) { 519 $a = new stdClass; 520 $a->count = $count; 521 $a->search = $search; 522 return array(get_string('toomanyusersmatchsearch', '', $a) => array(), 523 get_string('pleasesearchmore') => array()); 524 } else { 525 return array(get_string('toomanyuserstoshow', '', $count) => array(), 526 get_string('pleaseusesearch') => array()); 527 } 528 } 529 530 /** 531 * Output the list of <optgroup>s and <options>s that go inside the select. 532 * 533 * This method should do the same as the JavaScript method 534 * user_selector.prototype.handle_response. 535 * 536 * @param array $groupedusers an array, as returned by find_users. 537 * @param string $search 538 * @return string HTML code. 539 */ 540 protected function output_options($groupedusers, $search) { 541 $output = ''; 542 543 // Ensure that the list of previously selected users is up to date. 544 $this->get_selected_users(); 545 546 // If $groupedusers is empty, make a 'no matching users' group. If there is 547 // only one selected user, set a flag to select them if that option is turned on. 548 $select = false; 549 if (empty($groupedusers)) { 550 if (!empty($search)) { 551 $groupedusers = array(get_string('nomatchingusers', '', $search) => array()); 552 } else { 553 $groupedusers = array(get_string('none') => array()); 554 } 555 } else if ($this->autoselectunique && count($groupedusers) == 1 && 556 count(reset($groupedusers)) == 1) { 557 $select = true; 558 if (!$this->multiselect) { 559 $this->selected = array(); 560 } 561 } 562 563 // Output each optgroup. 564 foreach ($groupedusers as $groupname => $users) { 565 $output .= $this->output_optgroup($groupname, $users, $select); 566 } 567 568 // If there were previously selected users who do not match the search, show them too. 569 if ($this->preserveselected && !empty($this->selected)) { 570 $output .= $this->output_optgroup(get_string('previouslyselectedusers', '', $search), $this->selected, true); 571 } 572 573 // This method trashes $this->selected, so clear the cache so it is rebuilt before anyone tried to use it again. 574 $this->selected = null; 575 576 return $output; 577 } 578 579 /** 580 * Output one particular optgroup. Used by the preceding function output_options. 581 * 582 * @param string $groupname the label for this optgroup. 583 * @param array $users the users to put in this optgroup. 584 * @param boolean $select if true, select the users in this group. 585 * @return string HTML code. 586 */ 587 protected function output_optgroup($groupname, $users, $select) { 588 if (!empty($users)) { 589 $output = ' <optgroup label="' . htmlspecialchars($groupname, ENT_COMPAT) . ' (' . count($users) . ')">' . "\n"; 590 foreach ($users as $user) { 591 $attributes = ''; 592 if (!empty($user->disabled)) { 593 $attributes .= ' disabled="disabled"'; 594 } else if ($select || isset($this->selected[$user->id])) { 595 $attributes .= ' selected="selected"'; 596 } 597 unset($this->selected[$user->id]); 598 $output .= ' <option' . $attributes . ' value="' . $user->id . '">' . 599 $this->output_user($user) . "</option>\n"; 600 if (!empty($user->infobelow)) { 601 // Poor man's indent here is because CSS styles do not work in select options, except in Firefox. 602 $output .= ' <option disabled="disabled" class="userselector-infobelow">' . 603 ' ' . s($user->infobelow) . '</option>'; 604 } 605 } 606 } else { 607 $output = ' <optgroup label="' . htmlspecialchars($groupname, ENT_COMPAT) . '">' . "\n"; 608 $output .= ' <option disabled="disabled"> </option>' . "\n"; 609 } 610 $output .= " </optgroup>\n"; 611 return $output; 612 } 613 614 /** 615 * Convert a user object to a string suitable for displaying as an option in the list box. 616 * 617 * @param object $user the user to display. 618 * @return string a string representation of the user. 619 */ 620 public function output_user($user) { 621 $out = fullname($user, $this->viewfullnames); 622 if ($this->extrafields) { 623 $displayfields = array(); 624 foreach ($this->extrafields as $field) { 625 $displayfields[] = s($user->{$field}); 626 } 627 $out .= ' (' . implode(', ', $displayfields) . ')'; 628 } 629 return $out; 630 } 631 632 /** 633 * Returns the string to use for the search button caption. 634 * 635 * @return string the caption for the search button. 636 */ 637 protected function search_button_caption() { 638 return get_string('search'); 639 } 640 641 /** 642 * Initialise one of the option checkboxes, either from the request, or failing that from the 643 * user_preferences table, or finally from the given default. 644 * 645 * @param string $name 646 * @param mixed $default 647 * @return mixed|null|string 648 */ 649 private function initialise_option($name, $default) { 650 $param = optional_param($name, null, PARAM_BOOL); 651 if (is_null($param)) { 652 return get_user_preferences($name, $default); 653 } else { 654 set_user_preference($name, $param); 655 return $param; 656 } 657 } 658 659 /** 660 * Output one of the options checkboxes. 661 * 662 * @param string $name 663 * @param string $on 664 * @param string $label 665 * @return string 666 */ 667 private function option_checkbox($name, $on, $label) { 668 if ($on) { 669 $checked = ' checked="checked"'; 670 } else { 671 $checked = ''; 672 } 673 $name = 'userselector_' . $name; 674 // For the benefit of brain-dead IE, the id must be different from the name of the hidden form field above. 675 // It seems that document.getElementById('frog') in IE will return and element with name="frog". 676 $output = '<div class="form-check"><input type="hidden" name="' . $name . '" value="0" />' . 677 '<label class="form-check-label" for="' . $name . 'id">' . 678 '<input class="form-check-input" type="checkbox" id="' . $name . 'id" name="' . $name . 679 '" value="1"' . $checked . ' /> ' . $label . 680 "</label> 681 </div>\n"; 682 user_preference_allow_ajax_update($name, PARAM_BOOL); 683 return $output; 684 } 685 686 /** 687 * Initialises JS for this control. 688 * 689 * @param string $search 690 * @return string any HTML needed here. 691 */ 692 protected function initialise_javascript($search) { 693 global $USER, $PAGE, $OUTPUT; 694 $output = ''; 695 696 // Put the options into the session, to allow search.php to respond to the ajax requests. 697 $options = $this->get_options(); 698 $hash = md5(serialize($options)); 699 $USER->userselectors[$hash] = $options; 700 701 // Initialise the selector. 702 $PAGE->requires->js_init_call( 703 'M.core_user.init_user_selector', 704 array($this->name, $hash, $this->extrafields, $search), 705 false, 706 self::$jsmodule 707 ); 708 return $output; 709 } 710 } 711 712 /** 713 * Base class to avoid duplicating code. 714 * 715 * @copyright 1999 onwards Martin Dougiamas http://dougiamas.com 716 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 717 */ 718 abstract class groups_user_selector_base extends user_selector_base { 719 /** @var int */ 720 protected $groupid; 721 /** @var int */ 722 protected $courseid; 723 724 /** 725 * Constructor. 726 * 727 * @param string $name control name 728 * @param array $options should have two elements with keys groupid and courseid. 729 */ 730 public function __construct($name, $options) { 731 global $CFG; 732 $options['accesscontext'] = context_course::instance($options['courseid']); 733 $options['includecustomfields'] = true; 734 parent::__construct($name, $options); 735 $this->groupid = $options['groupid']; 736 $this->courseid = $options['courseid']; 737 require_once($CFG->dirroot . '/group/lib.php'); 738 } 739 740 /** 741 * Returns options for this selector. 742 * @return array 743 */ 744 protected function get_options() { 745 $options = parent::get_options(); 746 $options['groupid'] = $this->groupid; 747 $options['courseid'] = $this->courseid; 748 return $options; 749 } 750 751 /** 752 * Creates an organised array from given data. 753 * 754 * @param array $roles array in the format returned by groups_calculate_role_people. 755 * @param string $search 756 * @return array array in the format find_users is supposed to return. 757 */ 758 protected function convert_array_format($roles, $search) { 759 if (empty($roles)) { 760 $roles = array(); 761 } 762 $groupedusers = array(); 763 foreach ($roles as $role) { 764 if ($search) { 765 $a = new stdClass; 766 $a->role = html_entity_decode($role->name, ENT_QUOTES, 'UTF-8'); 767 $a->search = $search; 768 $groupname = get_string('matchingsearchandrole', '', $a); 769 } else { 770 $groupname = html_entity_decode($role->name, ENT_QUOTES, 'UTF-8'); 771 } 772 $groupedusers[$groupname] = $role->users; 773 foreach ($groupedusers[$groupname] as &$user) { 774 unset($user->roles); 775 $user->fullname = fullname($user); 776 if (!empty($user->component)) { 777 $user->infobelow = get_string('addedby', 'group', 778 get_string('pluginname', $user->component)); 779 } 780 } 781 } 782 return $groupedusers; 783 } 784 } 785 786 /** 787 * User selector subclass for the list of users who are in a certain group. 788 * 789 * Used on the add group memebers page. 790 * 791 * @copyright 1999 onwards Martin Dougiamas http://dougiamas.com 792 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 793 */ 794 class group_members_selector extends groups_user_selector_base { 795 796 /** 797 * Finds users to display in this control. 798 * @param string $search 799 * @return array 800 */ 801 public function find_users($search) { 802 list($wherecondition, $params) = $this->search_sql($search, 'u'); 803 804 list($sort, $sortparams) = users_order_by_sql('u', $search, $this->accesscontext, $this->userfieldsmappings); 805 806 $roles = groups_get_members_by_role($this->groupid, $this->courseid, 807 $this->userfieldsselects . ', gm.component', 808 $sort, $wherecondition, array_merge($params, $sortparams, $this->userfieldsparams), $this->userfieldsjoin); 809 810 return $this->convert_array_format($roles, $search); 811 } 812 } 813 814 /** 815 * User selector subclass for the list of users who are not in a certain group. 816 * 817 * Used on the add group members page. 818 * 819 * @copyright 1999 onwards Martin Dougiamas http://dougiamas.com 820 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 821 */ 822 class group_non_members_selector extends groups_user_selector_base { 823 /** 824 * An array of user ids populated by find_users() used in print_user_summaries() 825 * @var array 826 */ 827 private $potentialmembersids = array(); 828 829 /** 830 * Output user. 831 * 832 * @param stdClass $user 833 * @return string 834 */ 835 public function output_user($user) { 836 return parent::output_user($user) . ' (' . $user->numgroups . ')'; 837 } 838 839 /** 840 * Returns the user selector JavaScript module 841 * @return array 842 */ 843 public function get_js_module() { 844 return self::$jsmodule; 845 } 846 847 /** 848 * Creates a global JS variable (userSummaries) that is used by the group selector 849 * to print related information when the user clicks on a user in the groups UI. 850 * 851 * Used by /group/clientlib.js 852 * 853 * @global moodle_page $PAGE 854 * @param int $courseid 855 */ 856 public function print_user_summaries($courseid) { 857 global $PAGE; 858 $usersummaries = $this->get_user_summaries($courseid); 859 $PAGE->requires->data_for_js('userSummaries', $usersummaries); 860 } 861 862 /** 863 * Construct HTML lists of group-memberships of the current set of users. 864 * 865 * Used in user/selector/search.php to repopulate the userSummaries JS global 866 * that is created in self::print_user_summaries() above. 867 * 868 * @param int $courseid The course 869 * @return string[] Array of HTML lists of groups. 870 */ 871 public function get_user_summaries($courseid) { 872 global $DB; 873 874 $usersummaries = array(); 875 876 // Get other groups user already belongs to. 877 $usergroups = array(); 878 $potentialmembersids = $this->potentialmembersids; 879 if (empty($potentialmembersids) == false) { 880 list($membersidsclause, $params) = $DB->get_in_or_equal($potentialmembersids, SQL_PARAMS_NAMED, 'pm'); 881 $sql = "SELECT u.id AS userid, g.* 882 FROM {user} u 883 JOIN {groups_members} gm ON u.id = gm.userid 884 JOIN {groups} g ON gm.groupid = g.id 885 WHERE u.id $membersidsclause AND g.courseid = :courseid "; 886 $params['courseid'] = $courseid; 887 $rs = $DB->get_recordset_sql($sql, $params); 888 foreach ($rs as $usergroup) { 889 $usergroups[$usergroup->userid][$usergroup->id] = $usergroup; 890 } 891 $rs->close(); 892 893 foreach ($potentialmembersids as $userid) { 894 if (isset($usergroups[$userid])) { 895 $usergrouplist = html_writer::start_tag('ul'); 896 foreach ($usergroups[$userid] as $groupitem) { 897 $usergrouplist .= html_writer::tag('li', format_string($groupitem->name)); 898 } 899 $usergrouplist .= html_writer::end_tag('ul'); 900 } else { 901 $usergrouplist = ''; 902 } 903 $usersummaries[] = $usergrouplist; 904 } 905 } 906 return $usersummaries; 907 } 908 909 /** 910 * Finds users to display in this control. 911 * 912 * @param string $search 913 * @return array 914 */ 915 public function find_users($search) { 916 global $DB; 917 918 // Get list of allowed roles. 919 $context = context_course::instance($this->courseid); 920 if ($validroleids = groups_get_possible_roles($context)) { 921 list($roleids, $roleparams) = $DB->get_in_or_equal($validroleids, SQL_PARAMS_NAMED, 'r'); 922 } else { 923 $roleids = " = -1"; 924 $roleparams = array(); 925 } 926 927 // We want to query both the current context and parent contexts. 928 list($relatedctxsql, $relatedctxparams) = 929 $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx'); 930 931 // Get the search condition. 932 list($searchcondition, $searchparams) = $this->search_sql($search, 'u'); 933 934 // Build the SQL. 935 $enrolledjoin = get_enrolled_join($context, 'u.id'); 936 937 $wheres = []; 938 $wheres[] = $enrolledjoin->wheres; 939 $wheres[] = 'u.deleted = 0'; 940 $wheres[] = 'gm.id IS NULL'; 941 $wheres = implode(' AND ', $wheres); 942 $wheres .= ' AND ' . $searchcondition; 943 944 $fields = "SELECT r.id AS roleid, u.id AS userid, 945 " . $this->userfieldsselects . ", 946 (SELECT count(igm.groupid) 947 FROM {groups_members} igm 948 JOIN {groups} ig ON igm.groupid = ig.id 949 WHERE igm.userid = u.id AND ig.courseid = :courseid) AS numgroups"; 950 $sql = " FROM {user} u 951 $enrolledjoin->joins 952 LEFT JOIN {role_assignments} ra ON (ra.userid = u.id AND ra.contextid $relatedctxsql AND ra.roleid $roleids) 953 LEFT JOIN {role} r ON r.id = ra.roleid 954 LEFT JOIN {groups_members} gm ON (gm.userid = u.id AND gm.groupid = :groupid) 955 $this->userfieldsjoin 956 WHERE $wheres"; 957 958 list($sort, $sortparams) = users_order_by_sql('u', $search, $this->accesscontext, $this->userfieldsmappings); 959 $orderby = ' ORDER BY ' . $sort; 960 961 $params = array_merge($searchparams, $roleparams, $relatedctxparams, $enrolledjoin->params, $this->userfieldsparams); 962 $params['courseid'] = $this->courseid; 963 $params['groupid'] = $this->groupid; 964 965 if (!$this->is_validating()) { 966 $potentialmemberscount = $DB->count_records_sql("SELECT COUNT(DISTINCT u.id) $sql", $params); 967 if ($potentialmemberscount > $this->maxusersperpage) { 968 return $this->too_many_results($search, $potentialmemberscount); 969 } 970 } 971 972 $rs = $DB->get_recordset_sql("$fields $sql $orderby", array_merge($params, $sortparams)); 973 $roles = groups_calculate_role_people($rs, $context); 974 975 // Don't hold onto user IDs if we're doing validation. 976 if (empty($this->validatinguserids) ) { 977 if ($roles) { 978 foreach ($roles as $k => $v) { 979 if ($v) { 980 foreach ($v->users as $uid => $userobject) { 981 $this->potentialmembersids[] = $uid; 982 } 983 } 984 } 985 } 986 } 987 988 return $this->convert_array_format($roles, $search); 989 } 990 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body