PageRenderTime 54ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/user/selector/lib.php

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