Commit | Line | Data |
---|---|---|
3564771d | 1 | <?php |
117bd748 PS |
2 | // This file is part of Moodle - http://moodle.org/ |
3 | // | |
3564771d | 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. | |
117bd748 | 13 | // |
3564771d | 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/>. | |
341b5ed2 | 16 | |
7cf1c7bd | 17 | /** |
18 | * Library of functions for database manipulation. | |
5930cded | 19 | * |
7cf1c7bd | 20 | * Other main libraries: |
21 | * - weblib.php - functions that produce web output | |
22 | * - moodlelib.php - general-purpose Moodle functions | |
3564771d | 23 | * |
78bfb562 | 24 | * @package core |
78bfb562 PS |
25 | * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} |
26 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
7cf1c7bd | 27 | */ |
28 | ||
78bfb562 PS |
29 | defined('MOODLE_INTERNAL') || die(); |
30 | ||
4f0c2d00 PS |
31 | /** |
32 | * The maximum courses in a category | |
33 | * MAX_COURSES_IN_CATEGORY * MAX_COURSE_CATEGORIES must not be more than max integer! | |
34 | */ | |
3564771d | 35 | define('MAX_COURSES_IN_CATEGORY', 10000); |
4f0c2d00 | 36 | |
117bd748 | 37 | /** |
3564771d | 38 | * The maximum number of course categories |
117bd748 | 39 | * MAX_COURSES_IN_CATEGORY * MAX_COURSE_CATEGORIES must not be more than max integer! |
3564771d | 40 | */ |
0cbe8111 | 41 | define('MAX_COURSE_CATEGORIES', 10000); |
42 | ||
4f0c2d00 PS |
43 | /** |
44 | * Number of seconds to wait before updating lastaccess information in DB. | |
0793389f DM |
45 | * |
46 | * We allow overwrites from config.php, useful to ensure coherence in performance | |
47 | * tests results. | |
4f0c2d00 | 48 | */ |
0793389f DM |
49 | if (!defined('LASTACCESS_UPDATE_SECS')) { |
50 | define('LASTACCESS_UPDATE_SECS', 60); | |
51 | } | |
df28d6c5 | 52 | |
18a97fd8 | 53 | /** |
fbc21ae8 | 54 | * Returns $user object of the main admin user |
3564771d | 55 | * |
5d6ab054 | 56 | * @static stdClass $mainadmin |
b739d51b | 57 | * @return stdClass {@link $USER} record from DB, false if not found |
fbc21ae8 | 58 | */ |
4f0c2d00 | 59 | function get_admin() { |
bb6ccfa5 PS |
60 | global $CFG, $DB; |
61 | ||
4f0c2d00 | 62 | static $mainadmin = null; |
3ed57516 | 63 | static $prevadmins = null; |
2965f8fd | 64 | |
c90e6b46 AD |
65 | if (empty($CFG->siteadmins)) { |
66 | // Should not happen on an ordinary site. | |
67 | // It does however happen during unit tests. | |
3ed57516 | 68 | return false; |
bb6ccfa5 PS |
69 | } |
70 | ||
3ed57516 PS |
71 | if (isset($mainadmin) and $prevadmins === $CFG->siteadmins) { |
72 | return clone($mainadmin); | |
b8d13bec PS |
73 | } |
74 | ||
3ed57516 PS |
75 | $mainadmin = null; |
76 | ||
bb6ccfa5 PS |
77 | foreach (explode(',', $CFG->siteadmins) as $id) { |
78 | if ($user = $DB->get_record('user', array('id'=>$id, 'deleted'=>0))) { | |
79 | $mainadmin = $user; | |
80 | break; | |
df28d6c5 | 81 | } |
df28d6c5 | 82 | } |
bb6ccfa5 PS |
83 | |
84 | if ($mainadmin) { | |
3ed57516 | 85 | $prevadmins = $CFG->siteadmins; |
bb6ccfa5 PS |
86 | return clone($mainadmin); |
87 | } else { | |
88 | // this should not happen | |
89 | return false; | |
90 | } | |
df28d6c5 | 91 | } |
92 | ||
18a97fd8 | 93 | /** |
4f0c2d00 | 94 | * Returns list of all admins, using 1 DB query |
fbc21ae8 | 95 | * |
3564771d | 96 | * @return array |
fbc21ae8 | 97 | */ |
df28d6c5 | 98 | function get_admins() { |
4f0c2d00 | 99 | global $DB, $CFG; |
5930cded | 100 | |
7928979b | 101 | if (empty($CFG->siteadmins)) { // Should not happen on an ordinary site |
adf176d7 | 102 | return array(); |
7928979b MD |
103 | } |
104 | ||
4f0c2d00 | 105 | $sql = "SELECT u.* |
624a690b | 106 | FROM {user} u |
4f0c2d00 | 107 | WHERE u.deleted = 0 AND u.id IN ($CFG->siteadmins)"; |
5930cded | 108 | |
3ed57516 PS |
109 | // We want the same order as in $CFG->siteadmins. |
110 | $records = $DB->get_records_sql($sql); | |
111 | $admins = array(); | |
112 | foreach (explode(',', $CFG->siteadmins) as $id) { | |
113 | $id = (int)$id; | |
114 | if (!isset($records[$id])) { | |
115 | // User does not exist, this should not happen. | |
116 | continue; | |
117 | } | |
118 | $admins[$records[$id]->id] = $records[$id]; | |
119 | } | |
120 | ||
121 | return $admins; | |
df28d6c5 | 122 | } |
123 | ||
900df8b6 | 124 | /** |
fbc21ae8 | 125 | * Search through course users |
126 | * | |
5930cded | 127 | * If $coursid specifies the site course then this function searches |
fbc21ae8 | 128 | * through all undeleted and confirmed users |
129 | * | |
3564771d | 130 | * @global object |
131 | * @uses SITEID | |
132 | * @uses SQL_PARAMS_NAMED | |
133 | * @uses CONTEXT_COURSE | |
fbc21ae8 | 134 | * @param int $courseid The course in question. |
135 | * @param int $groupid The group in question. | |
3564771d | 136 | * @param string $searchtext The string to search for |
137 | * @param string $sort A field to sort by | |
138 | * @param array $exceptions A list of IDs to ignore, eg 2,4,5,8,9,10 | |
139 | * @return array | |
fbc21ae8 | 140 | */ |
624a690b | 141 | function search_users($courseid, $groupid, $searchtext, $sort='', array $exceptions=null) { |
142 | global $DB; | |
0720313b | 143 | |
245ac557 | 144 | $fullname = $DB->sql_fullname('u.firstname', 'u.lastname'); |
8f0cd6ef | 145 | |
900df8b6 | 146 | if (!empty($exceptions)) { |
cf717dc2 | 147 | list($exceptions, $params) = $DB->get_in_or_equal($exceptions, SQL_PARAMS_NAMED, 'ex', false); |
624a690b | 148 | $except = "AND u.id $exceptions"; |
900df8b6 | 149 | } else { |
624a690b | 150 | $except = ""; |
151 | $params = array(); | |
900df8b6 | 152 | } |
2700d113 | 153 | |
900df8b6 | 154 | if (!empty($sort)) { |
624a690b | 155 | $order = "ORDER BY $sort"; |
900df8b6 | 156 | } else { |
624a690b | 157 | $order = ""; |
900df8b6 | 158 | } |
8f0cd6ef | 159 | |
b0238f06 | 160 | $select = "u.deleted = 0 AND u.confirmed = 1 AND (".$DB->sql_like($fullname, ':search1', false)." OR ".$DB->sql_like('u.email', ':search2', false).")"; |
624a690b | 161 | $params['search1'] = "%$searchtext%"; |
162 | $params['search2'] = "%$searchtext%"; | |
2700d113 | 163 | |
222ac91b | 164 | if (!$courseid or $courseid == SITEID) { |
624a690b | 165 | $sql = "SELECT u.id, u.firstname, u.lastname, u.email |
166 | FROM {user} u | |
167 | WHERE $select | |
168 | $except | |
169 | $order"; | |
170 | return $DB->get_records_sql($sql, $params); | |
2700d113 | 171 | |
624a690b | 172 | } else { |
900df8b6 | 173 | if ($groupid) { |
624a690b | 174 | $sql = "SELECT u.id, u.firstname, u.lastname, u.email |
175 | FROM {user} u | |
176 | JOIN {groups_members} gm ON gm.userid = u.id | |
177 | WHERE $select AND gm.groupid = :groupid | |
178 | $except | |
179 | $order"; | |
180 | $params['groupid'] = $groupid; | |
181 | return $DB->get_records_sql($sql, $params); | |
182 | ||
900df8b6 | 183 | } else { |
b0c6dc1c | 184 | $context = context_course::instance($courseid); |
4e829d48 MN |
185 | |
186 | // We want to query both the current context and parent contexts. | |
187 | list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx'); | |
624a690b | 188 | |
189 | $sql = "SELECT u.id, u.firstname, u.lastname, u.email | |
190 | FROM {user} u | |
191 | JOIN {role_assignments} ra ON ra.userid = u.id | |
4e829d48 | 192 | WHERE $select AND ra.contextid $relatedctxsql |
624a690b | 193 | $except |
194 | $order"; | |
4e829d48 | 195 | $params = array_merge($params, $relatedctxparams); |
624a690b | 196 | return $DB->get_records_sql($sql, $params); |
900df8b6 | 197 | } |
198 | } | |
df28d6c5 | 199 | } |
200 | ||
b2ec866f | 201 | /** |
202 | * Returns SQL used to search through user table to find users (in a query | |
203 | * which may also join and apply other conditions). | |
204 | * | |
205 | * You can combine this SQL with an existing query by adding 'AND $sql' to the | |
206 | * WHERE clause of your query (where $sql is the first element in the array | |
207 | * returned by this function), and merging in the $params array to the parameters | |
208 | * of your query (where $params is the second element). Your query should use | |
209 | * named parameters such as :param, rather than the question mark style. | |
210 | * | |
211 | * There are examples of basic usage in the unit test for this function. | |
212 | * | |
213 | * @param string $search the text to search for (empty string = find all) | |
214 | * @param string $u the table alias for the user table in the query being | |
215 | * built. May be ''. | |
216 | * @param bool $searchanywhere If true (default), searches in the middle of | |
217 | * names, otherwise only searches at start | |
218 | * @param array $extrafields Array of extra user fields to include in search | |
219 | * @param array $exclude Array of user ids to exclude (empty = don't exclude) | |
220 | * @param array $includeonly If specified, only returns users that have ids | |
221 | * incldued in this array (empty = don't restrict) | |
222 | * @return array an array with two elements, a fragment of SQL to go in the | |
223 | * where clause the query, and an associative array containing any required | |
224 | * parameters (using named placeholders). | |
225 | */ | |
226 | function users_search_sql($search, $u = 'u', $searchanywhere = true, array $extrafields = array(), | |
d2e32121 | 227 | array $exclude = null, array $includeonly = null) { |
b2ec866f | 228 | global $DB, $CFG; |
229 | $params = array(); | |
230 | $tests = array(); | |
231 | ||
232 | if ($u) { | |
233 | $u .= '.'; | |
234 | } | |
235 | ||
236 | // If we have a $search string, put a field LIKE '$search%' condition on each field. | |
237 | if ($search) { | |
238 | $conditions = array( | |
239 | $DB->sql_fullname($u . 'firstname', $u . 'lastname'), | |
240 | $conditions[] = $u . 'lastname' | |
241 | ); | |
242 | foreach ($extrafields as $field) { | |
243 | $conditions[] = $u . $field; | |
244 | } | |
245 | if ($searchanywhere) { | |
246 | $searchparam = '%' . $search . '%'; | |
247 | } else { | |
248 | $searchparam = $search . '%'; | |
249 | } | |
250 | $i = 0; | |
251 | foreach ($conditions as $key => $condition) { | |
252 | $conditions[$key] = $DB->sql_like($condition, ":con{$i}00", false, false); | |
253 | $params["con{$i}00"] = $searchparam; | |
254 | $i++; | |
255 | } | |
256 | $tests[] = '(' . implode(' OR ', $conditions) . ')'; | |
257 | } | |
258 | ||
259 | // Add some additional sensible conditions. | |
260 | $tests[] = $u . "id <> :guestid"; | |
261 | $params['guestid'] = $CFG->siteguest; | |
262 | $tests[] = $u . 'deleted = 0'; | |
263 | $tests[] = $u . 'confirmed = 1'; | |
264 | ||
265 | // If we are being asked to exclude any users, do that. | |
266 | if (!empty($exclude)) { | |
267 | list($usertest, $userparams) = $DB->get_in_or_equal($exclude, SQL_PARAMS_NAMED, 'ex', false); | |
268 | $tests[] = $u . 'id ' . $usertest; | |
269 | $params = array_merge($params, $userparams); | |
270 | } | |
271 | ||
272 | // If we are validating a set list of userids, add an id IN (...) test. | |
273 | if (!empty($includeonly)) { | |
274 | list($usertest, $userparams) = $DB->get_in_or_equal($includeonly, SQL_PARAMS_NAMED, 'val'); | |
275 | $tests[] = $u . 'id ' . $usertest; | |
276 | $params = array_merge($params, $userparams); | |
277 | } | |
278 | ||
279 | // In case there are no tests, add one result (this makes it easier to combine | |
280 | // this with an existing query as you can always add AND $sql). | |
281 | if (empty($tests)) { | |
282 | $tests[] = '1 = 1'; | |
283 | } | |
284 | ||
285 | // Combing the conditions and return. | |
286 | return array(implode(' AND ', $tests), $params); | |
287 | } | |
288 | ||
289 | ||
9f82ddd6 TH |
290 | /** |
291 | * This function generates the standard ORDER BY clause for use when generating | |
292 | * lists of users. If you don't have a reason to use a different order, then | |
293 | * you should use this method to generate the order when displaying lists of users. | |
294 | * | |
295 | * If the optional $search parameter is passed, then exact matches to the search | |
296 | * will be sorted first. For example, suppose you have two users 'Al Zebra' and | |
297 | * 'Alan Aardvark'. The default sort is Alan, then Al. If, however, you search for | |
298 | * 'Al', then Al will be listed first. (With two users, this is not a big deal, | |
299 | * but with thousands of users, it is essential.) | |
300 | * | |
301 | * The list of fields scanned for exact matches are: | |
302 | * - firstname | |
303 | * - lastname | |
304 | * - $DB->sql_fullname | |
305 | * - those returned by get_extra_user_fields | |
306 | * | |
307 | * If named parameters are used (which is the default, and highly recommended), | |
308 | * then the parameter names are like :usersortexactN, where N is an int. | |
309 | * | |
310 | * The simplest possible example use is: | |
311 | * list($sort, $params) = users_order_by_sql(); | |
312 | * $sql = 'SELECT * FROM {users} ORDER BY ' . $sort; | |
313 | * | |
314 | * A more complex example, showing that this sort can be combined with other sorts: | |
315 | * list($sort, $sortparams) = users_order_by_sql('u'); | |
316 | * $sql = "SELECT g.id AS groupid, gg.groupingid, u.id AS userid, u.firstname, u.lastname, u.idnumber, u.username | |
317 | * FROM {groups} g | |
318 | * LEFT JOIN {groupings_groups} gg ON g.id = gg.groupid | |
319 | * LEFT JOIN {groups_members} gm ON g.id = gm.groupid | |
320 | * LEFT JOIN {user} u ON gm.userid = u.id | |
321 | * WHERE g.courseid = :courseid $groupwhere $groupingwhere | |
322 | * ORDER BY g.name, $sort"; | |
323 | * $params += $sortparams; | |
324 | * | |
325 | * An example showing the use of $search: | |
326 | * list($sort, $sortparams) = users_order_by_sql('u', $search, $this->get_context()); | |
327 | * $order = ' ORDER BY ' . $sort; | |
328 | * $params += $sortparams; | |
329 | * $availableusers = $DB->get_records_sql($fields . $sql . $order, $params, $page*$perpage, $perpage); | |
330 | * | |
331 | * @param string $usertablealias (optional) any table prefix for the {users} table. E.g. 'u'. | |
332 | * @param string $search (optional) a current search string. If given, | |
333 | * any exact matches to this string will be sorted first. | |
334 | * @param context $context the context we are in. Use by get_extra_user_fields. | |
335 | * Defaults to $PAGE->context. | |
336 | * @return array with two elements: | |
337 | * string SQL fragment to use in the ORDER BY clause. For example, "firstname, lastname". | |
338 | * array of parameters used in the SQL fragment. | |
339 | */ | |
340 | function users_order_by_sql($usertablealias = '', $search = null, context $context = null) { | |
341 | global $DB, $PAGE; | |
342 | ||
343 | if ($usertablealias) { | |
344 | $tableprefix = $usertablealias . '.'; | |
345 | } else { | |
346 | $tableprefix = ''; | |
347 | } | |
348 | ||
349 | $sort = "{$tableprefix}lastname, {$tableprefix}firstname, {$tableprefix}id"; | |
350 | $params = array(); | |
351 | ||
352 | if (!$search) { | |
353 | return array($sort, $params); | |
354 | } | |
355 | ||
356 | if (!$context) { | |
357 | $context = $PAGE->context; | |
358 | } | |
359 | ||
360 | $exactconditions = array(); | |
361 | $paramkey = 'usersortexact1'; | |
362 | ||
363 | $exactconditions[] = $DB->sql_fullname($tableprefix . 'firstname', $tableprefix . 'lastname') . | |
364 | ' = :' . $paramkey; | |
365 | $params[$paramkey] = $search; | |
366 | $paramkey++; | |
367 | ||
368 | $fieldstocheck = array_merge(array('firstname', 'lastname'), get_extra_user_fields($context)); | |
369 | foreach ($fieldstocheck as $key => $field) { | |
c2004a79 | 370 | $exactconditions[] = 'LOWER(' . $tableprefix . $field . ') = LOWER(:' . $paramkey . ')'; |
9f82ddd6 TH |
371 | $params[$paramkey] = $search; |
372 | $paramkey++; | |
373 | } | |
374 | ||
375 | $sort = 'CASE WHEN ' . implode(' OR ', $exactconditions) . | |
376 | ' THEN 0 ELSE 1 END, ' . $sort; | |
377 | ||
378 | return array($sort, $params); | |
379 | } | |
380 | ||
18a97fd8 | 381 | /** |
fbc21ae8 | 382 | * Returns a subset of users |
383 | * | |
3564771d | 384 | * @global object |
385 | * @uses DEBUG_DEVELOPER | |
386 | * @uses SQL_PARAMS_NAMED | |
7290c7fa | 387 | * @param bool $get If false then only a count of the records is returned |
fbc21ae8 | 388 | * @param string $search A simple string to search for |
7290c7fa | 389 | * @param bool $confirmed A switch to allow/disallow unconfirmed users |
3564771d | 390 | * @param array $exceptions A list of IDs to ignore, eg 2,4,5,8,9,10 |
fbc21ae8 | 391 | * @param string $sort A SQL snippet for the sorting criteria to use |
3564771d | 392 | * @param string $firstinitial Users whose first name starts with $firstinitial |
393 | * @param string $lastinitial Users whose last name starts with $lastinitial | |
394 | * @param string $page The page or records to return | |
395 | * @param string $recordsperpage The number of records to return per page | |
fbc21ae8 | 396 | * @param string $fields A comma separated list of fields to be returned from the chosen table. |
117bd748 | 397 | * @return array|int|bool {@link $USER} records unless get is false in which case the integer count of the records found is returned. |
9f82ddd6 | 398 | * False is returned if an error is encountered. |
fbc21ae8 | 399 | */ |
624a690b | 400 | function get_users($get=true, $search='', $confirmed=false, array $exceptions=null, $sort='firstname ASC', |
401 | $firstinitial='', $lastinitial='', $page='', $recordsperpage='', $fields='*', $extraselect='', array $extraparams=null) { | |
b3df1764 | 402 | global $DB, $CFG; |
5930cded | 403 | |
36075e09 | 404 | if ($get && !$recordsperpage) { |
405 | debugging('Call to get_users with $get = true no $recordsperpage limit. ' . | |
406 | 'On large installations, this will probably cause an out of memory error. ' . | |
407 | 'Please think again and change your code so that it does not try to ' . | |
03517306 | 408 | 'load so much data into memory.', DEBUG_DEVELOPER); |
36075e09 | 409 | } |
18a97fd8 | 410 | |
245ac557 | 411 | $fullname = $DB->sql_fullname(); |
e384fb7b | 412 | |
b3df1764 PS |
413 | $select = " id <> :guestid AND deleted = 0"; |
414 | $params = array('guestid'=>$CFG->siteguest); | |
488acd1b | 415 | |
0044147e | 416 | if (!empty($search)){ |
417 | $search = trim($search); | |
b0238f06 | 418 | $select .= " AND (".$DB->sql_like($fullname, ':search1', false)." OR ".$DB->sql_like('email', ':search2', false)." OR username = :search3)"; |
624a690b | 419 | $params['search1'] = "%$search%"; |
420 | $params['search2'] = "%$search%"; | |
421 | $params['search3'] = "$search"; | |
e384fb7b | 422 | } |
423 | ||
5a741655 | 424 | if ($confirmed) { |
624a690b | 425 | $select .= " AND confirmed = 1"; |
5a741655 | 426 | } |
427 | ||
428 | if ($exceptions) { | |
cf717dc2 | 429 | list($exceptions, $eparams) = $DB->get_in_or_equal($exceptions, SQL_PARAMS_NAMED, 'ex', false); |
624a690b | 430 | $params = $params + $eparams; |
cdca666b | 431 | $select .= " AND id $exceptions"; |
5a741655 | 432 | } |
433 | ||
488acd1b | 434 | if ($firstinitial) { |
b0238f06 | 435 | $select .= " AND ".$DB->sql_like('firstname', ':fni', false, false); |
624a690b | 436 | $params['fni'] = "$firstinitial%"; |
8f0cd6ef | 437 | } |
488acd1b | 438 | if ($lastinitial) { |
b0238f06 | 439 | $select .= " AND ".$DB->sql_like('lastname', ':lni', false, false); |
624a690b | 440 | $params['lni'] = "$lastinitial%"; |
8f0cd6ef | 441 | } |
488acd1b | 442 | |
cd1edf9e | 443 | if ($extraselect) { |
624a690b | 444 | $select .= " AND $extraselect"; |
445 | $params = $params + (array)$extraparams; | |
cd1edf9e | 446 | } |
447 | ||
5a741655 | 448 | if ($get) { |
624a690b | 449 | return $DB->get_records_select('user', $select, $params, $sort, $fields, $page, $recordsperpage); |
5a741655 | 450 | } else { |
624a690b | 451 | return $DB->count_records_select('user', $select, $params); |
5a741655 | 452 | } |
9fa49e22 | 453 | } |
454 | ||
5a741655 | 455 | |
18a97fd8 | 456 | /** |
043f1005 | 457 | * Return filtered (if provided) list of users in site, except guest and deleted users. |
3564771d | 458 | * |
459 | * @param string $sort An SQL field to sort by | |
460 | * @param string $dir The sort direction ASC|DESC | |
461 | * @param int $page The page or records to return | |
462 | * @param int $recordsperpage The number of records to return per page | |
463 | * @param string $search A simple string to search for | |
464 | * @param string $firstinitial Users whose first name starts with $firstinitial | |
465 | * @param string $lastinitial Users whose last name starts with $lastinitial | |
466 | * @param string $extraselect An additional SQL select statement to append to the query | |
467 | * @param array $extraparams Additional parameters to use for the above $extraselect | |
043f1005 | 468 | * @param stdClass $extracontext If specified, will include user 'extra fields' |
1fdc0c6a | 469 | * as appropriate for current user and given context |
3564771d | 470 | * @return array Array of {@link $USER} records |
fbc21ae8 | 471 | */ |
36075e09 | 472 | function get_users_listing($sort='lastaccess', $dir='ASC', $page=0, $recordsperpage=0, |
1fdc0c6a | 473 | $search='', $firstinitial='', $lastinitial='', $extraselect='', |
474 | array $extraparams=null, $extracontext = null) { | |
4e0b615e | 475 | global $DB, $CFG; |
31fefa63 | 476 | |
245ac557 | 477 | $fullname = $DB->sql_fullname(); |
c2a96d6b | 478 | |
4e0b615e RT |
479 | $select = "deleted <> 1 AND id <> :guestid"; |
480 | $params = array('guestid' => $CFG->siteguest); | |
488acd1b | 481 | |
0044147e | 482 | if (!empty($search)) { |
483 | $search = trim($search); | |
f63ac65a PS |
484 | $select .= " AND (". $DB->sql_like($fullname, ':search1', false, false). |
485 | " OR ". $DB->sql_like('email', ':search2', false, false). | |
486 | " OR username = :search3)"; | |
624a690b | 487 | $params['search1'] = "%$search%"; |
488 | $params['search2'] = "%$search%"; | |
489 | $params['search3'] = "$search"; | |
488acd1b | 490 | } |
491 | ||
492 | if ($firstinitial) { | |
f63ac65a | 493 | $select .= " AND ". $DB->sql_like('firstname', ':fni', false, false); |
624a690b | 494 | $params['fni'] = "$firstinitial%"; |
488acd1b | 495 | } |
488acd1b | 496 | if ($lastinitial) { |
f63ac65a | 497 | $select .= " AND ". $DB->sql_like('lastname', ':lni', false, false); |
624a690b | 498 | $params['lni'] = "$lastinitial%"; |
c750592a | 499 | } |
500 | ||
cd1edf9e | 501 | if ($extraselect) { |
624a690b | 502 | $select .= " AND $extraselect"; |
503 | $params = $params + (array)$extraparams; | |
cd1edf9e | 504 | } |
03d820c7 | 505 | |
488acd1b | 506 | if ($sort) { |
624a690b | 507 | $sort = " ORDER BY $sort $dir"; |
488acd1b | 508 | } |
509 | ||
1fdc0c6a | 510 | // If a context is specified, get extra user fields that the current user |
511 | // is supposed to see. | |
512 | $extrafields = ''; | |
513 | if ($extracontext) { | |
514 | $extrafields = get_extra_user_fields_sql($extracontext, '', '', | |
515 | array('id', 'username', 'email', 'firstname', 'lastname', 'city', 'country', | |
516 | 'lastaccess', 'confirmed', 'mnethostid')); | |
517 | } | |
a327f25e AG |
518 | $namefields = get_all_user_name_fields(true); |
519 | $extrafields = "$extrafields, $namefields"; | |
1fdc0c6a | 520 | |
521 | // warning: will return UNCONFIRMED USERS | |
a327f25e | 522 | return $DB->get_records_sql("SELECT id, username, email, city, country, lastaccess, confirmed, mnethostid, suspended $extrafields |
624a690b | 523 | FROM {user} |
524 | WHERE $select | |
525 | $sort", $params, $page, $recordsperpage); | |
9fa49e22 | 526 | |
527 | } | |
528 | ||
488acd1b | 529 | |
18a97fd8 | 530 | /** |
7290c7fa | 531 | * Full list of users that have confirmed their accounts. |
fbc21ae8 | 532 | * |
3564771d | 533 | * @global object |
624a690b | 534 | * @return array of unconfirmed users |
fbc21ae8 | 535 | */ |
9fa49e22 | 536 | function get_users_confirmed() { |
b3df1764 | 537 | global $DB, $CFG; |
624a690b | 538 | return $DB->get_records_sql("SELECT * |
539 | FROM {user} | |
b3df1764 | 540 | WHERE confirmed = 1 AND deleted = 0 AND id <> ?", array($CFG->siteguest)); |
9fa49e22 | 541 | } |
542 | ||
543 | ||
02ebf404 | 544 | /// OTHER SITE AND COURSE FUNCTIONS ///////////////////////////////////////////// |
545 | ||
546 | ||
18a97fd8 | 547 | /** |
fbc21ae8 | 548 | * Returns $course object of the top-level site. |
549 | * | |
3f77c158 | 550 | * @return object A {@link $COURSE} object for the site, exception if not found |
fbc21ae8 | 551 | */ |
c44d5d42 | 552 | function get_site() { |
624a690b | 553 | global $SITE, $DB; |
c44d5d42 | 554 | |
555 | if (!empty($SITE->id)) { // We already have a global to use, so return that | |
556 | return $SITE; | |
557 | } | |
02ebf404 | 558 | |
624a690b | 559 | if ($course = $DB->get_record('course', array('category'=>0))) { |
02ebf404 | 560 | return $course; |
561 | } else { | |
3f77c158 PS |
562 | // course table exists, but the site is not there, |
563 | // unfortunately there is no automatic way to recover | |
564 | throw new moodle_exception('nosite', 'error'); | |
02ebf404 | 565 | } |
566 | } | |
567 | ||
a2602457 | 568 | /** |
569 | * Gets a course object from database. If the course id corresponds to an | |
570 | * already-loaded $COURSE or $SITE object, then the loaded object will be used, | |
571 | * saving a database query. | |
572 | * | |
573 | * If it reuses an existing object, by default the object will be cloned. This | |
574 | * means you can modify the object safely without affecting other code. | |
575 | * | |
576 | * @param int $courseid Course id | |
577 | * @param bool $clone If true (default), makes a clone of the record | |
578 | * @return stdClass A course object | |
579 | * @throws dml_exception If not found in database | |
580 | */ | |
581 | function get_course($courseid, $clone = true) { | |
582 | global $DB, $COURSE, $SITE; | |
583 | if (!empty($COURSE->id) && $COURSE->id == $courseid) { | |
584 | return $clone ? clone($COURSE) : $COURSE; | |
585 | } else if (!empty($SITE->id) && $SITE->id == $courseid) { | |
586 | return $clone ? clone($SITE) : $SITE; | |
587 | } else { | |
588 | return $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST); | |
589 | } | |
590 | } | |
591 | ||
18a97fd8 | 592 | /** |
613bbd7c | 593 | * Returns list of courses, for whole site, or category |
594 | * | |
595 | * Returns list of courses, for whole site, or category | |
bfbfdb53 | 596 | * Important: Using c.* for fields is extremely expensive because |
613bbd7c | 597 | * we are using distinct. You almost _NEVER_ need all the fields |
598 | * in such a large SELECT | |
599 | * | |
3564771d | 600 | * @global object |
601 | * @global object | |
602 | * @global object | |
603 | * @uses CONTEXT_COURSE | |
604 | * @param string|int $categoryid Either a category id or 'all' for everything | |
605 | * @param string $sort A field and direction to sort by | |
606 | * @param string $fields The additional fields to return | |
607 | * @return array Array of courses | |
613bbd7c | 608 | */ |
6315b1c8 | 609 | function get_courses($categoryid="all", $sort="c.sortorder ASC", $fields="c.*") { |
02ebf404 | 610 | |
3b8a284c | 611 | global $USER, $CFG, $DB; |
5930cded | 612 | |
3b8a284c | 613 | $params = array(); |
614 | ||
615 | if ($categoryid !== "all" && is_numeric($categoryid)) { | |
616 | $categoryselect = "WHERE c.category = :catid"; | |
617 | $params['catid'] = $categoryid; | |
71dea306 | 618 | } else { |
5930cded | 619 | $categoryselect = ""; |
09575480 | 620 | } |
621 | ||
622 | if (empty($sort)) { | |
623 | $sortstatement = ""; | |
624 | } else { | |
625 | $sortstatement = "ORDER BY $sort"; | |
626 | } | |
627 | ||
628 | $visiblecourses = array(); | |
5930cded | 629 | |
2e4c0c91 FM |
630 | $ccselect = ', ' . context_helper::get_preload_record_columns_sql('ctx'); |
631 | $ccjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)"; | |
632 | $params['contextlevel'] = CONTEXT_COURSE; | |
4f0c2d00 PS |
633 | |
634 | $sql = "SELECT $fields $ccselect | |
3b8a284c | 635 | FROM {course} c |
4f0c2d00 | 636 | $ccjoin |
3b8a284c | 637 | $categoryselect |
638 | $sortstatement"; | |
639 | ||
71dea306 | 640 | // pull out all course matching the cat |
3b8a284c | 641 | if ($courses = $DB->get_records_sql($sql, $params)) { |
09575480 | 642 | |
643 | // loop throught them | |
644 | foreach ($courses as $course) { | |
db314f34 | 645 | context_helper::preload_from_record($course); |
285f94f5 | 646 | if (isset($course->visible) && $course->visible <= 0) { |
09575480 | 647 | // for hidden courses, require visibility check |
b0c6dc1c | 648 | if (has_capability('moodle/course:viewhiddencourses', context_course::instance($course->id))) { |
3b8a284c | 649 | $visiblecourses [$course->id] = $course; |
09575480 | 650 | } |
651 | } else { | |
3b8a284c | 652 | $visiblecourses [$course->id] = $course; |
5930cded | 653 | } |
09575480 | 654 | } |
6315b1c8 | 655 | } |
71dea306 | 656 | return $visiblecourses; |
8130b77b | 657 | } |
658 | ||
8130b77b | 659 | |
6315b1c8 | 660 | /** |
613bbd7c | 661 | * Returns list of courses, for whole site, or category |
662 | * | |
663 | * Similar to get_courses, but allows paging | |
5930cded | 664 | * Important: Using c.* for fields is extremely expensive because |
613bbd7c | 665 | * we are using distinct. You almost _NEVER_ need all the fields |
666 | * in such a large SELECT | |
667 | * | |
3564771d | 668 | * @global object |
669 | * @global object | |
670 | * @global object | |
671 | * @uses CONTEXT_COURSE | |
672 | * @param string|int $categoryid Either a category id or 'all' for everything | |
673 | * @param string $sort A field and direction to sort by | |
674 | * @param string $fields The additional fields to return | |
675 | * @param int $totalcount Reference for the number of courses | |
676 | * @param string $limitfrom The course to start from | |
677 | * @param string $limitnum The number of courses to limit to | |
117bd748 | 678 | * @return array Array of courses |
613bbd7c | 679 | */ |
6315b1c8 | 680 | function get_courses_page($categoryid="all", $sort="c.sortorder ASC", $fields="c.*", |
681 | &$totalcount, $limitfrom="", $limitnum="") { | |
3b8a284c | 682 | global $USER, $CFG, $DB; |
c7fe5c6f | 683 | |
3b8a284c | 684 | $params = array(); |
5930cded | 685 | |
71dea306 | 686 | $categoryselect = ""; |
2c49fb4c | 687 | if ($categoryid !== "all" && is_numeric($categoryid)) { |
3b8a284c | 688 | $categoryselect = "WHERE c.category = :catid"; |
689 | $params['catid'] = $categoryid; | |
71dea306 | 690 | } else { |
5930cded | 691 | $categoryselect = ""; |
692 | } | |
693 | ||
2e4c0c91 FM |
694 | $ccselect = ', ' . context_helper::get_preload_record_columns_sql('ctx'); |
695 | $ccjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)"; | |
696 | $params['contextlevel'] = CONTEXT_COURSE; | |
4f0c2d00 | 697 | |
afa559e9 EL |
698 | $totalcount = 0; |
699 | if (!$limitfrom) { | |
700 | $limitfrom = 0; | |
701 | } | |
702 | $visiblecourses = array(); | |
703 | ||
4f0c2d00 | 704 | $sql = "SELECT $fields $ccselect |
3b8a284c | 705 | FROM {course} c |
4f0c2d00 | 706 | $ccjoin |
3b8a284c | 707 | $categoryselect |
708 | ORDER BY $sort"; | |
709 | ||
71dea306 | 710 | // pull out all course matching the cat |
afa559e9 | 711 | $rs = $DB->get_recordset_sql($sql, $params); |
71dea306 | 712 | // iteration will have to be done inside loop to keep track of the limitfrom and limitnum |
3b8a284c | 713 | foreach($rs as $course) { |
db314f34 | 714 | context_helper::preload_from_record($course); |
03cedd62 | 715 | if ($course->visible <= 0) { |
716 | // for hidden courses, require visibility check | |
b0c6dc1c | 717 | if (has_capability('moodle/course:viewhiddencourses', context_course::instance($course->id))) { |
71dea306 | 718 | $totalcount++; |
03cedd62 | 719 | if ($totalcount > $limitfrom && (!$limitnum or count($visiblecourses) < $limitnum)) { |
3b8a284c | 720 | $visiblecourses [$course->id] = $course; |
71dea306 | 721 | } |
722 | } | |
03cedd62 | 723 | } else { |
724 | $totalcount++; | |
725 | if ($totalcount > $limitfrom && (!$limitnum or count($visiblecourses) < $limitnum)) { | |
3b8a284c | 726 | $visiblecourses [$course->id] = $course; |
03cedd62 | 727 | } |
5930cded | 728 | } |
71dea306 | 729 | } |
3b8a284c | 730 | $rs->close(); |
71dea306 | 731 | return $visiblecourses; |
02ebf404 | 732 | } |
733 | ||
18a97fd8 | 734 | /** |
7290c7fa | 735 | * A list of courses that match a search |
fbc21ae8 | 736 | * |
3564771d | 737 | * @global object |
738 | * @global object | |
739 | * @param array $searchterms An array of search criteria | |
740 | * @param string $sort A field and direction to sort by | |
741 | * @param int $page The page number to get | |
742 | * @param int $recordsperpage The number of records per page | |
743 | * @param int $totalcount Passed in by reference. | |
235ef57a | 744 | * @param array $requiredcapabilities Extra list of capabilities used to filter courses |
7290c7fa | 745 | * @return object {@link $COURSE} records |
fbc21ae8 | 746 | */ |
235ef57a DW |
747 | function get_courses_search($searchterms, $sort, $page, $recordsperpage, &$totalcount, |
748 | $requiredcapabilities = array()) { | |
3b8a284c | 749 | global $CFG, $DB; |
02ebf404 | 750 | |
06c1a1da | 751 | if ($DB->sql_regex_supported()) { |
752 | $REGEXP = $DB->sql_regex(true); | |
753 | $NOTREGEXP = $DB->sql_regex(false); | |
02ebf404 | 754 | } |
755 | ||
06c1a1da | 756 | $searchcond = array(); |
757 | $params = array(); | |
758 | $i = 0; | |
02ebf404 | 759 | |
86e00f97 EL |
760 | // Thanks Oracle for your non-ansi concat and type limits in coalesce. MDL-29912 |
761 | if ($DB->get_dbfamily() == 'oracle') { | |
cc1e4332 | 762 | $concat = "(c.summary|| ' ' || c.fullname || ' ' || c.idnumber || ' ' || c.shortname)"; |
86e00f97 | 763 | } else { |
cc1e4332 | 764 | $concat = $DB->sql_concat("COALESCE(c.summary, '')", "' '", 'c.fullname', "' '", 'c.idnumber', "' '", 'c.shortname'); |
86e00f97 | 765 | } |
3b8a284c | 766 | |
02ebf404 | 767 | foreach ($searchterms as $searchterm) { |
06c1a1da | 768 | $i++; |
6bb0f67f | 769 | |
b0238f06 | 770 | $NOT = false; /// Initially we aren't going to perform NOT LIKE searches, only MSSQL and Oracle |
0f62a5b5 | 771 | /// will use it to simulate the "-" operator with LIKE clause |
772 | ||
6bb0f67f | 773 | /// Under Oracle and MSSQL, trim the + and - operators and perform |
0f62a5b5 | 774 | /// simpler LIKE (or NOT LIKE) queries |
06c1a1da | 775 | if (!$DB->sql_regex_supported()) { |
0f62a5b5 | 776 | if (substr($searchterm, 0, 1) == '-') { |
b0238f06 | 777 | $NOT = true; |
0f62a5b5 | 778 | } |
6bb0f67f | 779 | $searchterm = trim($searchterm, '+-'); |
780 | } | |
781 | ||
06c1a1da | 782 | // TODO: +- may not work for non latin languages |
3b8a284c | 783 | |
d4419d55 | 784 | if (substr($searchterm,0,1) == '+') { |
06c1a1da | 785 | $searchterm = trim($searchterm, '+-'); |
786 | $searchterm = preg_quote($searchterm, '|'); | |
787 | $searchcond[] = "$concat $REGEXP :ss$i"; | |
788 | $params['ss'.$i] = "(^|[^a-zA-Z0-9])$searchterm([^a-zA-Z0-9]|$)"; | |
789 | ||
a8b56716 | 790 | } else if (substr($searchterm,0,1) == "-") { |
06c1a1da | 791 | $searchterm = trim($searchterm, '+-'); |
792 | $searchterm = preg_quote($searchterm, '|'); | |
793 | $searchcond[] = "$concat $NOTREGEXP :ss$i"; | |
794 | $params['ss'.$i] = "(^|[^a-zA-Z0-9])$searchterm([^a-zA-Z0-9]|$)"; | |
795 | ||
a8b56716 | 796 | } else { |
47586394 | 797 | $searchcond[] = $DB->sql_like($concat,":ss$i", false, true, $NOT); |
06c1a1da | 798 | $params['ss'.$i] = "%$searchterm%"; |
a8b56716 | 799 | } |
02ebf404 | 800 | } |
801 | ||
06c1a1da | 802 | if (empty($searchcond)) { |
235ef57a | 803 | $searchcond = array('1 = 1'); |
06c1a1da | 804 | } |
805 | ||
806 | $searchcond = implode(" AND ", $searchcond); | |
807 | ||
afa559e9 EL |
808 | $courses = array(); |
809 | $c = 0; // counts how many visible courses we've seen | |
810 | ||
811 | // Tiki pagination | |
812 | $limitfrom = $page * $recordsperpage; | |
813 | $limitto = $limitfrom + $recordsperpage; | |
814 | ||
2e4c0c91 FM |
815 | $ccselect = ', ' . context_helper::get_preload_record_columns_sql('ctx'); |
816 | $ccjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)"; | |
817 | $params['contextlevel'] = CONTEXT_COURSE; | |
818 | ||
4a3fb71c | 819 | $sql = "SELECT c.* $ccselect |
3b8a284c | 820 | FROM {course} c |
4f0c2d00 | 821 | $ccjoin |
06c1a1da | 822 | WHERE $searchcond AND c.id <> ".SITEID." |
823 | ORDER BY $sort"; | |
2c64f65c | 824 | |
afa559e9 EL |
825 | $rs = $DB->get_recordset_sql($sql, $params); |
826 | foreach($rs as $course) { | |
235ef57a DW |
827 | // Preload contexts only for hidden courses or courses we need to return. |
828 | context_helper::preload_from_record($course); | |
829 | $coursecontext = context_course::instance($course->id); | |
830 | if (!$course->visible && !has_capability('moodle/course:viewhiddencourses', $coursecontext)) { | |
831 | continue; | |
832 | } | |
833 | if (!empty($requiredcapabilities)) { | |
834 | if (!has_all_capabilities($requiredcapabilities, $coursecontext)) { | |
93c544bd | 835 | continue; |
02ebf404 | 836 | } |
837 | } | |
93c544bd MG |
838 | // Don't exit this loop till the end |
839 | // we need to count all the visible courses | |
840 | // to update $totalcount | |
841 | if ($c >= $limitfrom && $c < $limitto) { | |
842 | $courses[$course->id] = $course; | |
843 | } | |
844 | $c++; | |
02ebf404 | 845 | } |
afa559e9 | 846 | $rs->close(); |
02ebf404 | 847 | |
2c64f65c | 848 | // our caller expects 2 bits of data - our return |
849 | // array, and an updated $totalcount | |
850 | $totalcount = $c; | |
02ebf404 | 851 | return $courses; |
852 | } | |
853 | ||
0cbe8111 | 854 | /** |
855 | * Fixes course category and course sortorder, also verifies category and course parents and paths. | |
a1b892cc | 856 | * (circular references are not fixed) |
3564771d | 857 | * |
858 | * @global object | |
859 | * @global object | |
860 | * @uses MAX_COURSES_IN_CATEGORY | |
861 | * @uses MAX_COURSE_CATEGORIES | |
862 | * @uses SITEID | |
863 | * @uses CONTEXT_COURSE | |
864 | * @return void | |
0cbe8111 | 865 | */ |
866 | function fix_course_sortorder() { | |
867 | global $DB, $SITE; | |
868 | ||
869 | //WARNING: this is PHP5 only code! | |
870 | ||
eabbfa82 MG |
871 | // if there are any changes made to courses or categories we will trigger |
872 | // the cache events to purge all cached courses/categories data | |
873 | $cacheevents = array(); | |
874 | ||
0cbe8111 | 875 | if ($unsorted = $DB->get_records('course_categories', array('sortorder'=>0))) { |
876 | //move all categories that are not sorted yet to the end | |
877 | $DB->set_field('course_categories', 'sortorder', MAX_COURSES_IN_CATEGORY*MAX_COURSE_CATEGORIES, array('sortorder'=>0)); | |
eabbfa82 | 878 | $cacheevents['changesincoursecat'] = true; |
0cbe8111 | 879 | } |
880 | ||
881 | $allcats = $DB->get_records('course_categories', null, 'sortorder, id', 'id, sortorder, parent, depth, path'); | |
882 | $topcats = array(); | |
883 | $brokencats = array(); | |
884 | foreach ($allcats as $cat) { | |
885 | $sortorder = (int)$cat->sortorder; | |
886 | if (!$cat->parent) { | |
887 | while(isset($topcats[$sortorder])) { | |
888 | $sortorder++; | |
889 | } | |
890 | $topcats[$sortorder] = $cat; | |
891 | continue; | |
892 | } | |
893 | if (!isset($allcats[$cat->parent])) { | |
894 | $brokencats[] = $cat; | |
895 | continue; | |
c5d13b68 | 896 | } |
0cbe8111 | 897 | if (!isset($allcats[$cat->parent]->children)) { |
898 | $allcats[$cat->parent]->children = array(); | |
c5d13b68 | 899 | } |
0cbe8111 | 900 | while(isset($allcats[$cat->parent]->children[$sortorder])) { |
901 | $sortorder++; | |
902 | } | |
903 | $allcats[$cat->parent]->children[$sortorder] = $cat; | |
f41ef63e | 904 | } |
0cbe8111 | 905 | unset($allcats); |
39f65595 | 906 | |
0cbe8111 | 907 | // add broken cats to category tree |
908 | if ($brokencats) { | |
909 | $defaultcat = reset($topcats); | |
910 | foreach ($brokencats as $cat) { | |
911 | $topcats[] = $cat; | |
a1b892cc | 912 | } |
ba87a4da | 913 | } |
914 | ||
0cbe8111 | 915 | // now walk recursively the tree and fix any problems found |
916 | $sortorder = 0; | |
917 | $fixcontexts = array(); | |
eabbfa82 MG |
918 | if (_fix_course_cats($topcats, $sortorder, 0, 0, '', $fixcontexts)) { |
919 | $cacheevents['changesincoursecat'] = true; | |
920 | } | |
0cbe8111 | 921 | |
922 | // detect if there are "multiple" frontpage courses and fix them if needed | |
923 | $frontcourses = $DB->get_records('course', array('category'=>0), 'id'); | |
924 | if (count($frontcourses) > 1) { | |
925 | if (isset($frontcourses[SITEID])) { | |
926 | $frontcourse = $frontcourses[SITEID]; | |
927 | unset($frontcourses[SITEID]); | |
928 | } else { | |
929 | $frontcourse = array_shift($frontcourses); | |
930 | } | |
931 | $defaultcat = reset($topcats); | |
932 | foreach ($frontcourses as $course) { | |
933 | $DB->set_field('course', 'category', $defaultcat->id, array('id'=>$course->id)); | |
b0c6dc1c | 934 | $context = context_course::instance($course->id); |
0cbe8111 | 935 | $fixcontexts[$context->id] = $context; |
eabbfa82 | 936 | $cacheevents['changesincourse'] = true; |
0cbe8111 | 937 | } |
938 | unset($frontcourses); | |
939 | } else { | |
940 | $frontcourse = reset($frontcourses); | |
814748c9 | 941 | } |
942 | ||
0cbe8111 | 943 | // now fix the paths and depths in context table if needed |
944 | if ($fixcontexts) { | |
e922fe23 PS |
945 | foreach ($fixcontexts as $fixcontext) { |
946 | $fixcontext->reset_paths(false); | |
947 | } | |
948 | context_helper::build_all_paths(false); | |
949 | unset($fixcontexts); | |
eabbfa82 MG |
950 | $cacheevents['changesincourse'] = true; |
951 | $cacheevents['changesincoursecat'] = true; | |
39f65595 | 952 | } |
5930cded | 953 | |
0cbe8111 | 954 | // release memory |
955 | unset($topcats); | |
956 | unset($brokencats); | |
957 | unset($fixcontexts); | |
958 | ||
959 | // fix frontpage course sortorder | |
960 | if ($frontcourse->sortorder != 1) { | |
961 | $DB->set_field('course', 'sortorder', 1, array('id'=>$frontcourse->id)); | |
eabbfa82 | 962 | $cacheevents['changesincourse'] = true; |
39f65595 | 963 | } |
964 | ||
0cbe8111 | 965 | // now fix the course counts in category records if needed |
966 | $sql = "SELECT cc.id, cc.coursecount, COUNT(c.id) AS newcount | |
967 | FROM {course_categories} cc | |
968 | LEFT JOIN {course} c ON c.category = cc.id | |
969 | GROUP BY cc.id, cc.coursecount | |
970 | HAVING cc.coursecount <> COUNT(c.id)"; | |
ba87a4da | 971 | |
0cbe8111 | 972 | if ($updatecounts = $DB->get_records_sql($sql)) { |
3acbe663 DC |
973 | // categories with more courses than MAX_COURSES_IN_CATEGORY |
974 | $categories = array(); | |
0cbe8111 | 975 | foreach ($updatecounts as $cat) { |
976 | $cat->coursecount = $cat->newcount; | |
3acbe663 DC |
977 | if ($cat->coursecount >= MAX_COURSES_IN_CATEGORY) { |
978 | $categories[] = $cat->id; | |
979 | } | |
0cbe8111 | 980 | unset($cat->newcount); |
981 | $DB->update_record_raw('course_categories', $cat, true); | |
a1b892cc | 982 | } |
3acbe663 DC |
983 | if (!empty($categories)) { |
984 | $str = implode(', ', $categories); | |
985 | debugging("The number of courses (category id: $str) has reached MAX_COURSES_IN_CATEGORY (" . MAX_COURSES_IN_CATEGORY . "), it will cause a sorting performance issue, please increase the value of MAX_COURSES_IN_CATEGORY in lib/datalib.php file. See tracker issue: MDL-25669", DEBUG_DEVELOPER); | |
986 | } | |
eabbfa82 | 987 | $cacheevents['changesincoursecat'] = true; |
02ebf404 | 988 | } |
8f0cd6ef | 989 | |
0cbe8111 | 990 | // now make sure that sortorders in course table are withing the category sortorder ranges |
8ed5dd63 | 991 | $sql = "SELECT DISTINCT cc.id, cc.sortorder |
0cbe8111 | 992 | FROM {course_categories} cc |
993 | JOIN {course} c ON c.category = cc.id | |
994 | WHERE c.sortorder < cc.sortorder OR c.sortorder > cc.sortorder + ".MAX_COURSES_IN_CATEGORY; | |
995 | ||
996 | if ($fixcategories = $DB->get_records_sql($sql)) { | |
997 | //fix the course sortorder ranges | |
998 | foreach ($fixcategories as $cat) { | |
999 | $sql = "UPDATE {course} | |
78a0635c | 1000 | SET sortorder = ".$DB->sql_modulo('sortorder', MAX_COURSES_IN_CATEGORY)." + ? |
0cbe8111 | 1001 | WHERE category = ?"; |
1002 | $DB->execute($sql, array($cat->sortorder, $cat->id)); | |
1003 | } | |
eabbfa82 | 1004 | $cacheevents['changesincoursecat'] = true; |
814748c9 | 1005 | } |
0cbe8111 | 1006 | unset($fixcategories); |
1007 | ||
1008 | // categories having courses with sortorder duplicates or having gaps in sortorder | |
1009 | $sql = "SELECT DISTINCT c1.category AS id , cc.sortorder | |
1010 | FROM {course} c1 | |
1011 | JOIN {course} c2 ON c1.sortorder = c2.sortorder | |
1012 | JOIN {course_categories} cc ON (c1.category = cc.id) | |
1013 | WHERE c1.id <> c2.id"; | |
1014 | $fixcategories = $DB->get_records_sql($sql); | |
1015 | ||
1016 | $sql = "SELECT cc.id, cc.sortorder, cc.coursecount, MAX(c.sortorder) AS maxsort, MIN(c.sortorder) AS minsort | |
1017 | FROM {course_categories} cc | |
1018 | JOIN {course} c ON c.category = cc.id | |
1019 | GROUP BY cc.id, cc.sortorder, cc.coursecount | |
1020 | HAVING (MAX(c.sortorder) <> cc.sortorder + cc.coursecount) OR (MIN(c.sortorder) <> cc.sortorder + 1)"; | |
1021 | $gapcategories = $DB->get_records_sql($sql); | |
1022 | ||
1023 | foreach ($gapcategories as $cat) { | |
1024 | if (isset($fixcategories[$cat->id])) { | |
1025 | // duplicates detected already | |
1026 | ||
1027 | } else if ($cat->minsort == $cat->sortorder and $cat->maxsort == $cat->sortorder + $cat->coursecount - 1) { | |
1028 | // easy - new course inserted with sortorder 0, the rest is ok | |
1029 | $sql = "UPDATE {course} | |
1030 | SET sortorder = sortorder + 1 | |
1031 | WHERE category = ?"; | |
1032 | $DB->execute($sql, array($cat->id)); | |
758b9a4d | 1033 | |
0cbe8111 | 1034 | } else { |
1035 | // it needs full resorting | |
1036 | $fixcategories[$cat->id] = $cat; | |
6bc502cc | 1037 | } |
eabbfa82 | 1038 | $cacheevents['changesincourse'] = true; |
6bc502cc | 1039 | } |
0cbe8111 | 1040 | unset($gapcategories); |
8f0cd6ef | 1041 | |
0cbe8111 | 1042 | // fix course sortorders in problematic categories only |
1043 | foreach ($fixcategories as $cat) { | |
1044 | $i = 1; | |
1045 | $courses = $DB->get_records('course', array('category'=>$cat->id), 'sortorder ASC, id DESC', 'id, sortorder'); | |
1046 | foreach ($courses as $course) { | |
1047 | if ($course->sortorder != $cat->sortorder + $i) { | |
1048 | $course->sortorder = $cat->sortorder + $i; | |
a1b892cc | 1049 | $DB->update_record_raw('course', $course, true); |
eabbfa82 | 1050 | $cacheevents['changesincourse'] = true; |
0cbe8111 | 1051 | } |
1052 | $i++; | |
1053 | } | |
1054 | } | |
b33389d2 | 1055 | |
eabbfa82 MG |
1056 | // advise all caches that need to be rebuilt |
1057 | foreach (array_keys($cacheevents) as $event) { | |
1058 | cache_helper::purge_by_event($event); | |
1059 | } | |
02ebf404 | 1060 | } |
1061 | ||
d8634192 | 1062 | /** |
0cbe8111 | 1063 | * Internal recursive category verification function, do not use directly! |
3564771d | 1064 | * |
1065 | * @todo Document the arguments of this function better | |
1066 | * | |
1067 | * @global object | |
1068 | * @uses MAX_COURSES_IN_CATEGORY | |
1069 | * @uses CONTEXT_COURSECAT | |
1070 | * @param array $children | |
1071 | * @param int $sortorder | |
1072 | * @param string $parent | |
1073 | * @param int $depth | |
1074 | * @param string $path | |
1075 | * @param array $fixcontexts | |
eabbfa82 | 1076 | * @return bool if changes were made |
0cbe8111 | 1077 | */ |
1078 | function _fix_course_cats($children, &$sortorder, $parent, $depth, $path, &$fixcontexts) { | |
c3df0901 | 1079 | global $DB; |
d8634192 | 1080 | |
0cbe8111 | 1081 | $depth++; |
eabbfa82 | 1082 | $changesmade = false; |
c3df0901 | 1083 | |
0cbe8111 | 1084 | foreach ($children as $cat) { |
1085 | $sortorder = $sortorder + MAX_COURSES_IN_CATEGORY; | |
1086 | $update = false; | |
1087 | if ($parent != $cat->parent or $depth != $cat->depth or $path.'/'.$cat->id != $cat->path) { | |
1088 | $cat->parent = $parent; | |
1089 | $cat->depth = $depth; | |
1090 | $cat->path = $path.'/'.$cat->id; | |
1091 | $update = true; | |
c3df0901 | 1092 | |
0cbe8111 | 1093 | // make sure context caches are rebuild and dirty contexts marked |
b0c6dc1c | 1094 | $context = context_coursecat::instance($cat->id); |
0cbe8111 | 1095 | $fixcontexts[$context->id] = $context; |
1096 | } | |
1097 | if ($cat->sortorder != $sortorder) { | |
1098 | $cat->sortorder = $sortorder; | |
1099 | $update = true; | |
1100 | } | |
1101 | if ($update) { | |
1102 | $DB->update_record('course_categories', $cat, true); | |
eabbfa82 | 1103 | $changesmade = true; |
0cbe8111 | 1104 | } |
1105 | if (isset($cat->children)) { | |
eabbfa82 MG |
1106 | if (_fix_course_cats($cat->children, $sortorder, $cat->id, $cat->depth, $cat->path, $fixcontexts)) { |
1107 | $changesmade = true; | |
1108 | } | |
d8634192 | 1109 | } |
1110 | } | |
eabbfa82 | 1111 | return $changesmade; |
d8634192 | 1112 | } |
1113 | ||
db4b12eb | 1114 | /** |
1115 | * List of remote courses that a user has access to via MNET. | |
1116 | * Works only on the IDP | |
1117 | * | |
3564771d | 1118 | * @global object |
1119 | * @global object | |
1120 | * @param int @userid The user id to get remote courses for | |
1121 | * @return array Array of {@link $COURSE} of course objects | |
db4b12eb | 1122 | */ |
1123 | function get_my_remotecourses($userid=0) { | |
c3df0901 | 1124 | global $DB, $USER; |
db4b12eb | 1125 | |
1126 | if (empty($userid)) { | |
1127 | $userid = $USER->id; | |
1128 | } | |
1129 | ||
c6575bef DM |
1130 | // we can not use SELECT DISTINCT + text field (summary) because of MS SQL and Oracle, subselect used therefore |
1131 | $sql = "SELECT c.id, c.remoteid, c.shortname, c.fullname, | |
152a2273 | 1132 | c.hostid, c.summary, c.summaryformat, c.categoryname AS cat_name, |
86dd62a7 | 1133 | h.name AS hostname |
152a2273 | 1134 | FROM {mnetservice_enrol_courses} c |
c6575bef DM |
1135 | JOIN (SELECT DISTINCT hostid, remotecourseid |
1136 | FROM {mnetservice_enrol_enrolments} | |
1137 | WHERE userid = ? | |
1138 | ) e ON (e.hostid = c.hostid AND e.remotecourseid = c.remoteid) | |
1139 | JOIN {mnet_host} h ON h.id = c.hostid"; | |
db4b12eb | 1140 | |
c3df0901 | 1141 | return $DB->get_records_sql($sql, array($userid)); |
db4b12eb | 1142 | } |
1143 | ||
1144 | /** | |
1145 | * List of remote hosts that a user has access to via MNET. | |
1146 | * Works on the SP | |
1147 | * | |
3564771d | 1148 | * @global object |
1149 | * @global object | |
1150 | * @return array|bool Array of host objects or false | |
db4b12eb | 1151 | */ |
1152 | function get_my_remotehosts() { | |
1153 | global $CFG, $USER; | |
1154 | ||
1155 | if ($USER->mnethostid == $CFG->mnet_localhost_id) { | |
1156 | return false; // Return nothing on the IDP | |
1157 | } | |
1158 | if (!empty($USER->mnet_foreign_host_array) && is_array($USER->mnet_foreign_host_array)) { | |
1159 | return $USER->mnet_foreign_host_array; | |
1160 | } | |
1161 | return false; | |
1162 | } | |
fbc21ae8 | 1163 | |
18a97fd8 | 1164 | /** |
fbc21ae8 | 1165 | * This function creates a default separated/connected scale |
1166 | * | |
1167 | * This function creates a default separated/connected scale | |
1168 | * so there's something in the database. The locations of | |
1169 | * strings and files is a bit odd, but this is because we | |
1170 | * need to maintain backward compatibility with many different | |
1171 | * existing language translations and older sites. | |
3564771d | 1172 | * |
1173 | * @global object | |
3564771d | 1174 | * @return void |
fbc21ae8 | 1175 | */ |
02ebf404 | 1176 | function make_default_scale() { |
2d0f7da8 | 1177 | global $DB; |
02ebf404 | 1178 | |
b85b25eb | 1179 | $defaultscale = new stdClass(); |
02ebf404 | 1180 | $defaultscale->courseid = 0; |
1181 | $defaultscale->userid = 0; | |
d4419d55 | 1182 | $defaultscale->name = get_string('separateandconnected'); |
2d0f7da8 | 1183 | $defaultscale->description = get_string('separateandconnectedinfo'); |
d4419d55 | 1184 | $defaultscale->scale = get_string('postrating1', 'forum').','. |
1185 | get_string('postrating2', 'forum').','. | |
1186 | get_string('postrating3', 'forum'); | |
02ebf404 | 1187 | $defaultscale->timemodified = time(); |
1188 | ||
a9637e7d PS |
1189 | $defaultscale->id = $DB->insert_record('scale', $defaultscale); |
1190 | $DB->execute("UPDATE {forum} SET scale = ?", array($defaultscale->id)); | |
02ebf404 | 1191 | } |
1192 | ||
fbc21ae8 | 1193 | |
18a97fd8 | 1194 | /** |
fbc21ae8 | 1195 | * Returns a menu of all available scales from the site as well as the given course |
1196 | * | |
3564771d | 1197 | * @global object |
fbc21ae8 | 1198 | * @param int $courseid The id of the course as found in the 'course' table. |
3564771d | 1199 | * @return array |
fbc21ae8 | 1200 | */ |
02ebf404 | 1201 | function get_scales_menu($courseid=0) { |
c3df0901 | 1202 | global $DB; |
02ebf404 | 1203 | |
c3df0901 | 1204 | $sql = "SELECT id, name |
1205 | FROM {scale} | |
1206 | WHERE courseid = 0 or courseid = ? | |
02ebf404 | 1207 | ORDER BY courseid ASC, name ASC"; |
c3df0901 | 1208 | $params = array($courseid); |
02ebf404 | 1209 | |
c3df0901 | 1210 | if ($scales = $DB->get_records_sql_menu($sql, $params)) { |
02ebf404 | 1211 | return $scales; |
1212 | } | |
1213 | ||
1214 | make_default_scale(); | |
1215 | ||
c3df0901 | 1216 | return $DB->get_records_sql_menu($sql, $params); |
02ebf404 | 1217 | } |
1218 | ||
d82993dc PS |
1219 | /** |
1220 | * Increment standard revision field. | |
1221 | * | |
1222 | * The revision are based on current time and are incrementing. | |
1223 | * There is a protection for runaway revisions, it may not go further than | |
1224 | * one hour into future. | |
1225 | * | |
1226 | * The field has to be XMLDB_TYPE_INTEGER with size 10. | |
1227 | * | |
1228 | * @param string $table | |
1229 | * @param string $field name of the field containing revision | |
1230 | * @param string $select use empty string when updating all records | |
1231 | * @param array $params optional select parameters | |
1232 | */ | |
1233 | function increment_revision_number($table, $field, $select, array $params = null) { | |
1234 | global $DB; | |
1235 | ||
1236 | $now = time(); | |
1237 | $sql = "UPDATE {{$table}} | |
1238 | SET $field = (CASE | |
1239 | WHEN $field IS NULL THEN $now | |
1240 | WHEN $field < $now THEN $now | |
1241 | WHEN $field > $now + 3600 THEN $now | |
1242 | ELSE $field + 1 END)"; | |
1243 | if ($select) { | |
1244 | $sql = $sql . " WHERE $select"; | |
1245 | } | |
1246 | $DB->execute($sql, $params); | |
1247 | } | |
1248 | ||
5baa0ad6 | 1249 | |
df28d6c5 | 1250 | /// MODULE FUNCTIONS ///////////////////////////////////////////////// |
1251 | ||
18a97fd8 | 1252 | /** |
fbc21ae8 | 1253 | * Just gets a raw list of all modules in a course |
1254 | * | |
3564771d | 1255 | * @global object |
fbc21ae8 | 1256 | * @param int $courseid The id of the course as found in the 'course' table. |
3564771d | 1257 | * @return array |
fbc21ae8 | 1258 | */ |
9fa49e22 | 1259 | function get_course_mods($courseid) { |
c3df0901 | 1260 | global $DB; |
9fa49e22 | 1261 | |
3a11c548 | 1262 | if (empty($courseid)) { |
1263 | return false; // avoid warnings | |
1264 | } | |
1265 | ||
c3df0901 | 1266 | return $DB->get_records_sql("SELECT cm.*, m.name as modname |
1267 | FROM {modules} m, {course_modules} cm | |
1268 | WHERE cm.course = ? AND cm.module = m.id AND m.visible = 1", | |
1269 | array($courseid)); // no disabled mods | |
9fa49e22 | 1270 | } |
1271 | ||
fbc21ae8 | 1272 | |
18a97fd8 | 1273 | /** |
f9d5371b | 1274 | * Given an id of a course module, finds the coursemodule description |
fbc21ae8 | 1275 | * |
92e2e855 MG |
1276 | * Please note that this function performs 1-2 DB queries. When possible use cached |
1277 | * course modinfo. For example get_fast_modinfo($courseorid)->get_cm($cmid) | |
1278 | * See also {@link cm_info::get_course_module_record()} | |
1279 | * | |
3564771d | 1280 | * @global object |
a1b892cc | 1281 | * @param string $modulename name of module type, eg. resource, assignment,... (optional, slower and less safe if not specified) |
f9d5371b | 1282 | * @param int $cmid course module id (id in course_modules table) |
1283 | * @param int $courseid optional course id for extra validation | |
a1b892cc | 1284 | * @param bool $sectionnum include relative section number (0,1,2 ...) |
4fea29e4 | 1285 | * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found; |
1286 | * IGNORE_MULTIPLE means return first, ignore multiple records found(not recommended); | |
1287 | * MUST_EXIST means throw exception if no record or multiple records found | |
83be47e6 | 1288 | * @return stdClass |
f9d5371b | 1289 | */ |
4fea29e4 | 1290 | function get_coursemodule_from_id($modulename, $cmid, $courseid=0, $sectionnum=false, $strictness=IGNORE_MISSING) { |
c3df0901 | 1291 | global $DB; |
f9d5371b | 1292 | |
a1b892cc | 1293 | $params = array('cmid'=>$cmid); |
1294 | ||
1295 | if (!$modulename) { | |
1296 | if (!$modulename = $DB->get_field_sql("SELECT md.name | |
1297 | FROM {modules} md | |
1298 | JOIN {course_modules} cm ON cm.module = md.id | |
4fea29e4 | 1299 | WHERE cm.id = :cmid", $params, $strictness)) { |
a1b892cc | 1300 | return false; |
1301 | } | |
b6f299bb PS |
1302 | } else { |
1303 | if (!core_component::is_valid_plugin_name('mod', $modulename)) { | |
1304 | throw new coding_exception('Invalid modulename parameter'); | |
1305 | } | |
a1b892cc | 1306 | } |
1307 | ||
1308 | $params['modulename'] = $modulename; | |
1309 | ||
d251907c | 1310 | $courseselect = ""; |
a1b892cc | 1311 | $sectionfield = ""; |
1312 | $sectionjoin = ""; | |
f9d5371b | 1313 | |
c3df0901 | 1314 | if ($courseid) { |
a1b892cc | 1315 | $courseselect = "AND cm.course = :courseid"; |
c3df0901 | 1316 | $params['courseid'] = $courseid; |
d251907c | 1317 | } |
c3df0901 | 1318 | |
a1b892cc | 1319 | if ($sectionnum) { |
1320 | $sectionfield = ", cw.section AS sectionnum"; | |
1321 | $sectionjoin = "LEFT JOIN {course_sections} cw ON cw.id = cm.section"; | |
1322 | } | |
1323 | ||
1324 | $sql = "SELECT cm.*, m.name, md.name AS modname $sectionfield | |
1325 | FROM {course_modules} cm | |
1326 | JOIN {modules} md ON md.id = cm.module | |
1327 | JOIN {".$modulename."} m ON m.id = cm.instance | |
1328 | $sectionjoin | |
1329 | WHERE cm.id = :cmid AND md.name = :modulename | |
1330 | $courseselect"; | |
1331 | ||
4fea29e4 | 1332 | return $DB->get_record_sql($sql, $params, $strictness); |
f9d5371b | 1333 | } |
1334 | ||
1335 | /** | |
1336 | * Given an instance number of a module, finds the coursemodule description | |
1337 | * | |
92e2e855 MG |
1338 | * Please note that this function performs DB query. When possible use cached course |
1339 | * modinfo. For example get_fast_modinfo($courseorid)->instances[$modulename][$instance] | |
1340 | * See also {@link cm_info::get_course_module_record()} | |
1341 | * | |
3564771d | 1342 | * @global object |
f9d5371b | 1343 | * @param string $modulename name of module type, eg. resource, assignment,... |
1344 | * @param int $instance module instance number (id in resource, assignment etc. table) | |
1345 | * @param int $courseid optional course id for extra validation | |
a1b892cc | 1346 | * @param bool $sectionnum include relative section number (0,1,2 ...) |
4fea29e4 | 1347 | * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found; |
1348 | * IGNORE_MULTIPLE means return first, ignore multiple records found(not recommended); | |
1349 | * MUST_EXIST means throw exception if no record or multiple records found | |
83be47e6 | 1350 | * @return stdClass |
fbc21ae8 | 1351 | */ |
4fea29e4 | 1352 | function get_coursemodule_from_instance($modulename, $instance, $courseid=0, $sectionnum=false, $strictness=IGNORE_MISSING) { |
c3df0901 | 1353 | global $DB; |
df28d6c5 | 1354 | |
b6f299bb PS |
1355 | if (!core_component::is_valid_plugin_name('mod', $modulename)) { |
1356 | throw new coding_exception('Invalid modulename parameter'); | |
1357 | } | |
1358 | ||
a1b892cc | 1359 | $params = array('instance'=>$instance, 'modulename'=>$modulename); |
1360 | ||
d251907c | 1361 | $courseselect = ""; |
a1b892cc | 1362 | $sectionfield = ""; |
1363 | $sectionjoin = ""; | |
df28d6c5 | 1364 | |
c3df0901 | 1365 | if ($courseid) { |
a1b892cc | 1366 | $courseselect = "AND cm.course = :courseid"; |
c3df0901 | 1367 | $params['courseid'] = $courseid; |
d251907c | 1368 | } |
c3df0901 | 1369 | |
a1b892cc | 1370 | if ($sectionnum) { |
1371 | $sectionfield = ", cw.section AS sectionnum"; | |
1372 | $sectionjoin = "LEFT JOIN {course_sections} cw ON cw.id = cm.section"; | |
1373 | } | |
1374 | ||
1375 | $sql = "SELECT cm.*, m.name, md.name AS modname $sectionfield | |
1376 | FROM {course_modules} cm | |
1377 | JOIN {modules} md ON md.id = cm.module | |
1378 | JOIN {".$modulename."} m ON m.id = cm.instance | |
1379 | $sectionjoin | |
1380 | WHERE m.id = :instance AND md.name = :modulename | |
1381 | $courseselect"; | |
df28d6c5 | 1382 | |
4fea29e4 | 1383 | return $DB->get_record_sql($sql, $params, $strictness); |
df28d6c5 | 1384 | } |
1385 | ||
dd97c328 | 1386 | /** |
1387 | * Returns all course modules of given activity in course | |
3564771d | 1388 | * |
1389 | * @param string $modulename The module name (forum, quiz, etc.) | |
1390 | * @param int $courseid The course id to get modules for | |
dd97c328 | 1391 | * @param string $extrafields extra fields starting with m. |
3564771d | 1392 | * @return array Array of results |
dd97c328 | 1393 | */ |
1394 | function get_coursemodules_in_course($modulename, $courseid, $extrafields='') { | |
c3df0901 | 1395 | global $DB; |
dd97c328 | 1396 | |
b6f299bb PS |
1397 | if (!core_component::is_valid_plugin_name('mod', $modulename)) { |
1398 | throw new coding_exception('Invalid modulename parameter'); | |
1399 | } | |
1400 | ||
dd97c328 | 1401 | if (!empty($extrafields)) { |
1402 | $extrafields = ", $extrafields"; | |
1403 | } | |
c3df0901 | 1404 | $params = array(); |
1405 | $params['courseid'] = $courseid; | |
1406 | $params['modulename'] = $modulename; | |
1407 | ||
1408 | ||
1409 | return $DB->get_records_sql("SELECT cm.*, m.name, md.name as modname $extrafields | |
1410 | FROM {course_modules} cm, {modules} md, {".$modulename."} m | |
1411 | WHERE cm.course = :courseid AND | |
1412 | cm.instance = m.id AND | |
1413 | md.name = :modulename AND | |
e0985504 | 1414 | md.id = cm.module", $params); |
dd97c328 | 1415 | } |
ac0b1a19 | 1416 | |
185cfb09 | 1417 | /** |
1418 | * Returns an array of all the active instances of a particular module in given courses, sorted in the order they are defined | |
1419 | * | |
1420 | * Returns an array of all the active instances of a particular | |
1421 | * module in given courses, sorted in the order they are defined | |
ac0b1a19 | 1422 | * in the course. Returns an empty array on any errors. |
185cfb09 | 1423 | * |
ac0b1a19 | 1424 | * The returned objects includle the columns cw.section, cm.visible, |
061e6b28 | 1425 | * cm.groupmode, and cm.groupingid, and are indexed by cm.id. |
ac0b1a19 | 1426 | * |
3564771d | 1427 | * @global object |
1428 | * @global object | |
ac0b1a19 | 1429 | * @param string $modulename The name of the module to get instances for |
1430 | * @param array $courses an array of course objects. | |
3564771d | 1431 | * @param int $userid |
1432 | * @param int $includeinvisible | |
ac0b1a19 | 1433 | * @return array of module instance objects, including some extra fields from the course_modules |
1434 | * and course_sections tables, or an empty array if an error occurred. | |
185cfb09 | 1435 | */ |
00e12c73 | 1436 | function get_all_instances_in_courses($modulename, $courses, $userid=NULL, $includeinvisible=false) { |
c3df0901 | 1437 | global $CFG, $DB; |
ac0b1a19 | 1438 | |
b6f299bb PS |
1439 | if (!core_component::is_valid_plugin_name('mod', $modulename)) { |
1440 | throw new coding_exception('Invalid modulename parameter'); | |
1441 | } | |
1442 | ||
ac0b1a19 | 1443 | $outputarray = array(); |
1444 | ||
185cfb09 | 1445 | if (empty($courses) || !is_array($courses) || count($courses) == 0) { |
ac0b1a19 | 1446 | return $outputarray; |
185cfb09 | 1447 | } |
ac0b1a19 | 1448 | |
c3df0901 | 1449 | list($coursessql, $params) = $DB->get_in_or_equal(array_keys($courses), SQL_PARAMS_NAMED, 'c0'); |
1450 | $params['modulename'] = $modulename; | |
1451 | ||
1452 | if (!$rawmods = $DB->get_records_sql("SELECT cm.id AS coursemodule, m.*, cw.section, cm.visible AS visible, | |
061e6b28 | 1453 | cm.groupmode, cm.groupingid |
c3df0901 | 1454 | FROM {course_modules} cm, {course_sections} cw, {modules} md, |
1455 | {".$modulename."} m | |
1456 | WHERE cm.course $coursessql AND | |
1457 | cm.instance = m.id AND | |
1458 | cm.section = cw.id AND | |
1459 | md.name = :modulename AND | |
1460 | md.id = cm.module", $params)) { | |
ac0b1a19 | 1461 | return $outputarray; |
185cfb09 | 1462 | } |
1463 | ||
185cfb09 | 1464 | foreach ($courses as $course) { |
ac0b1a19 | 1465 | $modinfo = get_fast_modinfo($course, $userid); |
fea43a7f | 1466 | |
ac0b1a19 | 1467 | if (empty($modinfo->instances[$modulename])) { |
185cfb09 | 1468 | continue; |
1469 | } | |
ac0b1a19 | 1470 | |
1471 | foreach ($modinfo->instances[$modulename] as $cm) { | |
1472 | if (!$includeinvisible and !$cm->uservisible) { | |
1473 | continue; | |
1474 | } | |
1475 | if (!isset($rawmods[$cm->id])) { | |
1476 | continue; | |
185cfb09 | 1477 | } |
ac0b1a19 | 1478 | $instance = $rawmods[$cm->id]; |
1479 | if (!empty($cm->extra)) { | |
9a9012dc | 1480 | $instance->extra = $cm->extra; |
ac0b1a19 | 1481 | } |
1482 | $outputarray[] = $instance; | |
185cfb09 | 1483 | } |
1484 | } | |
1485 | ||
1486 | return $outputarray; | |
185cfb09 | 1487 | } |
fbc21ae8 | 1488 | |
18a97fd8 | 1489 | /** |
3d96cba7 | 1490 | * Returns an array of all the active instances of a particular module in a given course, |
1491 | * sorted in the order they are defined. | |
fbc21ae8 | 1492 | * |
1493 | * Returns an array of all the active instances of a particular | |
1494 | * module in a given course, sorted in the order they are defined | |
3d96cba7 | 1495 | * in the course. Returns an empty array on any errors. |
1496 | * | |
1497 | * The returned objects includle the columns cw.section, cm.visible, | |
061e6b28 | 1498 | * cm.groupmode, and cm.groupingid, and are indexed by cm.id. |
fbc21ae8 | 1499 | * |
3564771d | 1500 | * Simply calls {@link all_instances_in_courses()} with a single provided course |
1501 | * | |
3d96cba7 | 1502 | * @param string $modulename The name of the module to get instances for |
ac0b1a19 | 1503 | * @param object $course The course obect. |
3d96cba7 | 1504 | * @return array of module instance objects, including some extra fields from the course_modules |
1505 | * and course_sections tables, or an empty array if an error occurred. | |
3564771d | 1506 | * @param int $userid |
1507 | * @param int $includeinvisible | |
fbc21ae8 | 1508 | */ |
00e12c73 | 1509 | function get_all_instances_in_course($modulename, $course, $userid=NULL, $includeinvisible=false) { |
ac0b1a19 | 1510 | return get_all_instances_in_courses($modulename, array($course->id => $course), $userid, $includeinvisible); |
df28d6c5 | 1511 | } |
1512 | ||
9fa49e22 | 1513 | |
18a97fd8 | 1514 | /** |
fbc21ae8 | 1515 | * Determine whether a module instance is visible within a course |
1516 | * | |
1517 | * Given a valid module object with info about the id and course, | |
1518 | * and the module's type (eg "forum") returns whether the object | |
061e6b28 | 1519 | * is visible or not according to the 'eye' icon only. |
fbc21ae8 | 1520 | * |
8d1f33e1 | 1521 | * NOTE: This does NOT take into account visibility to a particular user. |
1522 | * To get visibility access for a specific user, use get_fast_modinfo, get a | |
1523 | * cm_info object from this, and check the ->uservisible property; or use | |
1524 | * the \core_availability\info_module::is_user_visible() static function. | |
1525 | * | |
3564771d | 1526 | * @global object |
117bd748 | 1527 | |
613bbd7c | 1528 | * @param $moduletype Name of the module eg 'forum' |
1529 | * @param $module Object which is the instance of the module | |
3564771d | 1530 | * @return bool Success |
fbc21ae8 | 1531 | */ |
580f2fbc | 1532 | function instance_is_visible($moduletype, $module) { |
c3df0901 | 1533 | global $DB; |
580f2fbc | 1534 | |
2b49ae96 | 1535 | if (!empty($module->id)) { |
d251907c | 1536 | $params = array('courseid'=>$module->course, 'moduletype'=>$moduletype, 'moduleid'=>$module->id); |
061e6b28 | 1537 | if ($records = $DB->get_records_sql("SELECT cm.instance, cm.visible, cm.groupingid, cm.id, cm.course |
c3df0901 | 1538 | FROM {course_modules} cm, {modules} m |
1539 | WHERE cm.course = :courseid AND | |
1540 | cm.module = m.id AND | |
1541 | m.name = :moduletype AND | |
f93ea222 | 1542 | cm.instance = :moduleid", $params)) { |
5930cded | 1543 | |
2b49ae96 | 1544 | foreach ($records as $record) { // there should only be one - use the first one |
dd97c328 | 1545 | return $record->visible; |
2b49ae96 | 1546 | } |
580f2fbc | 1547 | } |
1548 | } | |
580f2fbc | 1549 | return true; // visible by default! |
1550 | } | |
1551 | ||
a3fb1c45 | 1552 | |
9fa49e22 | 1553 | /// LOG FUNCTIONS ///////////////////////////////////////////////////// |
1554 | ||
7eaca5a8 PŠ |
1555 | /** |
1556 | * Get instance of log manager. | |
1557 | * | |
1558 | * @param bool $forcereload | |
1559 | * @return \core\log\manager | |
1560 | */ | |
1561 | function get_log_manager($forcereload = false) { | |
1562 | /** @var \core\log\manager $singleton */ | |
1563 | static $singleton = null; | |
1564 | ||
1565 | if ($forcereload and isset($singleton)) { | |
1566 | $singleton->dispose(); | |
1567 | $singleton = null; | |
1568 | } | |
1569 | ||
1570 | if (isset($singleton)) { | |
1571 | return $singleton; | |
1572 | } | |
1573 | ||
1574 | $classname = '\tool_log\log\manager'; | |
1575 | if (defined('LOG_MANAGER_CLASS')) { | |
1576 | $classname = LOG_MANAGER_CLASS; | |
1577 | } | |
1578 | ||
1579 | if (!class_exists($classname)) { | |
1580 | if (!empty($classname)) { | |
1581 | debugging("Cannot find log manager class '$classname'.", DEBUG_DEVELOPER); | |
1582 | } | |
1583 | $classname = '\core\log\dummy_manager'; | |
1584 | } | |
1585 | ||
1586 | $singleton = new $classname(); | |
1587 | return $singleton; | |
1588 | } | |
1589 | ||
b8b50d04 AD |
1590 | /** |
1591 | * Add an entry to the config log table. | |
1592 | * | |
1593 | * These are "action" focussed rather than web server hits, | |
1594 | * and provide a way to easily reconstruct changes to Moodle configuration. | |
1595 | * | |
1596 | * @package core | |
1597 | * @category log | |
1598 | * @global moodle_database $DB | |
1599 | * @global stdClass $USER | |
1600 | * @param string $name The name of the configuration change action | |
1601 | For example 'filter_active' when activating or deactivating a filter | |
1602 | * @param string $oldvalue The config setting's previous value | |
1603 | * @param string $value The config setting's new value | |
1604 | * @param string $plugin Plugin name, for example a filter name when changing filter configuration | |
1605 | * @return void | |
1606 | */ | |
1607 | function add_to_config_log($name, $oldvalue, $value, $plugin) { | |
1608 | global $USER, $DB; | |
1609 | ||
1610 | $log = new stdClass(); | |
1611 | $log->userid = during_initial_install() ? 0 :$USER->id; // 0 as user id during install | |
1612 | $log->timemodified = time(); | |
1613 | $log->name = $name; | |
1614 | $log->oldvalue = $oldvalue; | |
1615 | $log->value = $value; | |
1616 | $log->plugin = $plugin; | |
1617 | $DB->insert_record('config_log', $log); | |
1618 | } | |
9fa49e22 | 1619 | |
341b5ed2 | 1620 | /** |
1621 | * Store user last access times - called when use enters a course or site | |
1622 | * | |
04252d3a AKA |
1623 | * @package core |
1624 | * @category log | |
1625 | * @global stdClass $USER | |
1626 | * @global stdClass $CFG | |
1627 | * @global moodle_database $DB | |
3564771d | 1628 | * @uses LASTACCESS_UPDATE_SECS |
1629 | * @uses SITEID | |
fcf64900 | 1630 | * @param int $courseid empty courseid means site |
341b5ed2 | 1631 | * @return void |
1632 | */ | |
1633 | function user_accesstime_log($courseid=0) { | |
f33e1ed4 | 1634 | global $USER, $CFG, $DB; |
341b5ed2 | 1635 | |
d79d5ac2 | 1636 | if (!isloggedin() or \core\session\manager::is_loggedinas()) { |
341b5ed2 | 1637 | // no access tracking |
1638 | return; | |
1639 | } | |
1640 | ||
4501a081 DP |
1641 | if (isguestuser()) { |
1642 | // Do not update guest access times/ips for performance. | |
1643 | return; | |
1644 | } | |
1645 | ||
341b5ed2 | 1646 | if (empty($courseid)) { |
1647 | $courseid = SITEID; | |
1648 | } | |
1649 | ||
1650 | $timenow = time(); | |
1651 | ||
1652 | /// Store site lastaccess time for the current user | |
1653 | if ($timenow - $USER->lastaccess > LASTACCESS_UPDATE_SECS) { | |
1654 | /// Update $USER->lastaccess for next checks | |
1655 | $USER->lastaccess = $timenow; | |
341b5ed2 | 1656 | |
365a5941 | 1657 | $last = new stdClass(); |
f33e1ed4 | 1658 | $last->id = $USER->id; |
1659 | $last->lastip = getremoteaddr(); | |
1660 | $last->lastaccess = $timenow; | |
1661 | ||
68fbad44 | 1662 | $DB->update_record_raw('user', $last); |
341b5ed2 | 1663 | } |
1664 | ||
1665 | if ($courseid == SITEID) { | |
1666 | /// no user_lastaccess for frontpage | |
1667 | return; | |
1668 | } | |
cb8aaedf | 1669 | |
341b5ed2 | 1670 | /// Store course lastaccess times for the current user |
1671 | if (empty($USER->currentcourseaccess[$courseid]) or ($timenow - $USER->currentcourseaccess[$courseid] > LASTACCESS_UPDATE_SECS)) { | |
341b5ed2 | 1672 | |
f33e1ed4 | 1673 | $lastaccess = $DB->get_field('user_lastaccess', 'timeaccess', array('userid'=>$USER->id, 'courseid'=>$courseid)); |
341b5ed2 | 1674 | |
f33e1ed4 | 1675 | if ($lastaccess === false) { |
1676 | // Update course lastaccess for next checks | |
1677 | $USER->currentcourseaccess[$courseid] = $timenow; | |
1678 | ||
365a5941 | 1679 | $last = new stdClass(); |
f33e1ed4 | 1680 | $last->userid = $USER->id; |
1681 | $last->courseid = $courseid; | |
1682 | $last->timeaccess = $timenow; | |
1d293ca4 RS |
1683 | try { |
1684 | $DB->insert_record_raw('user_lastaccess', $last, false); | |
1685 | } catch (dml_write_exception $e) { | |
1686 | // During a race condition we can fail to find the data, then it appears. | |
1687 | // If we still can't find it, rethrow the exception. | |
1688 | $lastaccess = $DB->get_field('user_lastaccess', 'timeaccess', array('userid' => $USER->id, | |
1689 | 'courseid' => $courseid)); | |
1690 | if ($lastaccess === false) { | |
1691 | throw $e; | |
1692 | } | |
1693 | // If we did find it, the race condition was true and another thread has inserted the time for us. | |
1694 | // We can just continue without having to do anything. | |
1695 | } | |
d251907c | 1696 | |
f33e1ed4 | 1697 | } else if ($timenow - $lastaccess < LASTACCESS_UPDATE_SECS) { |
1698 | // no need to update now, it was updated recently in concurrent login ;-) | |
341b5ed2 | 1699 | |
f33e1ed4 | 1700 | } else { |
1701 | // Update course lastaccess for next checks | |
1702 | $USER->currentcourseaccess[$courseid] = $timenow; | |
1703 | ||
68fbad44 | 1704 | $DB->set_field('user_lastaccess', 'timeaccess', $timenow, array('userid'=>$USER->id, 'courseid'=>$courseid)); |
3d94772d | 1705 | } |
8f0cd6ef | 1706 | } |
9fa49e22 | 1707 | } |
1708 | ||
18a97fd8 | 1709 | /** |
fbc21ae8 | 1710 | * Select all log records based on SQL criteria |
1711 | * | |
04252d3a AKA |
1712 | * @package core |
1713 | * @category log | |
1714 | * @global moodle_database $DB | |
fbc21ae8 | 1715 | * @param string $select SQL select criteria |
c3df0901 | 1716 | * @param array $params named sql type params |
fbc21ae8 | 1717 | * @param string $order SQL order by clause to sort the records returned |
fcf64900 AKA |
1718 | * @param string $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set) |
1719 | * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set) | |
fbc21ae8 | 1720 | * @param int $totalcount Passed in by reference. |
04252d3a | 1721 | * @return array |
fbc21ae8 | 1722 | */ |
c3df0901 | 1723 | function get_logs($select, array $params=null, $order='l.time DESC', $limitfrom='', $limitnum='', &$totalcount) { |
1724 | global $DB; | |
9fa49e22 | 1725 | |
519d369f | 1726 | if ($order) { |
c3df0901 | 1727 | $order = "ORDER BY $order"; |
1728 | } | |
1729 | ||
1730 | $selectsql = ""; | |
1731 | $countsql = ""; | |
1732 | ||
1733 | if ($select) { | |
1734 | $select = "WHERE $select"; | |
519d369f | 1735 | } |
1736 | ||
c3df0901 | 1737 | $sql = "SELECT COUNT(*) |
1738 | FROM {log} l | |
1739 | $select"; | |
1740 | ||
1741 | $totalcount = $DB->count_records_sql($sql, $params); | |
a327f25e AG |
1742 | $allnames = get_all_user_name_fields(true, 'u'); |
1743 | $sql = "SELECT l.*, $allnames, u.picture | |
d251907c | 1744 | FROM {log} l |
c3df0901 | 1745 | LEFT JOIN {user} u ON l.userid = u.id |
d251907c | 1746 | $select |
c3df0901 | 1747 | $order"; |
519d369f | 1748 | |
c3df0901 | 1749 | return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum) ; |
9fa49e22 | 1750 | } |
1751 | ||
519d369f | 1752 | |
18a97fd8 | 1753 | /** |
fbc21ae8 | 1754 | * Select all log records for a given course and user |
1755 | * | |
04252d3a AKA |
1756 | * @package core |
1757 | * @category log | |
1758 | * @global moodle_database $DB | |
2f87145b | 1759 | * @uses DAYSECS |
fbc21ae8 | 1760 | * @param int $userid The id of the user as found in the 'user' table. |
1761 | * @param int $courseid The id of the course as found in the 'course' table. | |
fcf64900 | 1762 | * @param string $coursestart unix timestamp representing course start date and time. |
04252d3a | 1763 | * @return array |
fbc21ae8 | 1764 | */ |
9fa49e22 | 1765 | function get_logs_usercourse($userid, $courseid, $coursestart) { |
c3df0901 | 1766 | global $DB; |
9fa49e22 | 1767 | |
c3df0901 | 1768 | $params = array(); |
1769 | ||
1770 | $courseselect = ''; | |
da0c90c3 | 1771 | if ($courseid) { |
c3df0901 | 1772 | $courseselect = "AND course = :courseid"; |
d251907c | 1773 | $params['courseid'] = $courseid; |
da0c90c3 | 1774 | } |
c3df0901 | 1775 | $params['userid'] = $userid; |
cdc6938b DM |
1776 | // We have to sanitize this param ourselves here instead of relying on DB. |
1777 | // Postgres complains if you use name parameter or column alias in GROUP BY. | |
1778 | // See MDL-27696 and 51c3e85 for details. | |
1779 | $coursestart = (int)$coursestart; | |
da0c90c3 | 1780 | |
51c3e855 | 1781 | return $DB->get_records_sql("SELECT FLOOR((time - $coursestart)/". DAYSECS .") AS day, COUNT(*) AS num |
c3df0901 | 1782 | FROM {log} |
1783 | WHERE userid = :userid | |
51c3e855 PS |
1784 | AND time > $coursestart $courseselect |
1785 | GROUP BY FLOOR((time - $coursestart)/". DAYSECS .")", $params); | |
9fa49e22 | 1786 | } |
1787 | ||
18a97fd8 | 1788 | /** |
fbc21ae8 | 1789 | * Select all log records for a given course, user, and day |
1790 | * | |
04252d3a AKA |
1791 | * @package core |
1792 | * @category log | |
1793 | * @global moodle_database $DB | |
2f87145b | 1794 | * @uses HOURSECS |
fbc21ae8 | 1795 | * @param int $userid The id of the user as found in the 'user' table. |
1796 | * @param int $courseid The id of the course as found in the 'course' table. | |
fcf64900 | 1797 | * @param string $daystart unix timestamp of the start of the day for which the logs needs to be retrived |
04252d3a | 1798 | * @return array |
fbc21ae8 | 1799 | */ |
9fa49e22 | 1800 | function get_logs_userday($userid, $courseid, $daystart) { |
c3df0901 | 1801 | global $DB; |
1802 | ||
51c3e855 | 1803 | $params = array('userid'=>$userid); |
9fa49e22 | 1804 | |
c3df0901 | 1805 | $courseselect = ''; |
7e4a6488 | 1806 | if ($courseid) { |
51c3e855 PS |
1807 | $courseselect = "AND course = :courseid"; |
1808 | $params['courseid'] = $courseid; | |
7e4a6488 | 1809 | } |
51c3e855 | 1810 | $daystart = (int)$daystart; // note: unfortunately pg complains if you use name parameter or column alias in GROUP BY |
7e4a6488 | 1811 | |
51c3e855 | 1812 | return $DB->get_records_sql("SELECT FLOOR((time - $daystart)/". HOURSECS .") AS hour, COUNT(*) AS num |
c3df0901 | 1813 | FROM {log} |
51c3e855 PS |
1814 | WHERE userid = :userid |
1815 | AND time > $daystart $courseselect | |
1816 | GROUP BY FLOOR((time - $daystart)/". HOURSECS .") ", $params); | |
9fa49e22 | 1817 | } |
1818 | ||
a3fb1c45 | 1819 | /// GENERAL HELPFUL THINGS /////////////////////////////////// |
1820 | ||
18a97fd8 | 1821 | /** |
68e550e6 | 1822 | * Dumps a given object's information for debugging purposes |
fbc21ae8 | 1823 | * |
68e550e6 DM |
1824 | * When used in a CLI script, the object's information is written to the standard |
1825 | * error output stream. When used in a web script, the object is dumped to a | |
1826 | * pre-formatted block with the "notifytiny" CSS class. | |
fbc21ae8 | 1827 | * |
1828 | * @param mixed $object The data to be printed | |
68e550e6 | 1829 | * @return void output is echo'd |
fbc21ae8 | 1830 | */ |
a3fb1c45 | 1831 | function print_object($object) { |
68e550e6 | 1832 | |
18767260 RT |
1833 | // we may need a lot of memory here |
1834 | raise_memory_limit(MEMORY_EXTRA); | |
68e550e6 DM |
1835 | |
1836 | if (CLI_SCRIPT) { | |
1837 | fwrite(STDERR, print_r($object, true)); | |
1838 | fwrite(STDERR, PHP_EOL); | |
1839 | } else { | |
1840 | echo html_writer::tag('pre', s(print_r($object, true)), array('class' => 'notifytiny')); | |
1841 | } | |
a3fb1c45 | 1842 | } |
1843 | ||
62d4e774 | 1844 | /** |
5930cded | 1845 | * This function is the official hook inside XMLDB stuff to delegate its debug to one |
62d4e774 | 1846 | * external function. |
1847 | * | |
1848 | * Any script can avoid calls to this function by defining XMLDB_SKIP_DEBUG_HOOK before | |
1849 | * using XMLDB classes. Obviously, also, if this function doesn't exist, it isn't invoked ;-) | |
1850 | * | |
3564771d | 1851 | * @uses DEBUG_DEVELOPER |
1852 | * @param string $message string contains the error message | |
1853 | * @param object $object object XMLDB object that fired the debug | |
62d4e774 | 1854 | */ |
1855 | function xmldb_debug($message, $object) { | |
1856 | ||
92b564f4 | 1857 | debugging($message, DEBUG_DEVELOPER); |
62d4e774 | 1858 | } |
1859 | ||
49860445 | 1860 | /** |
3564771d | 1861 | * @global object |
1862 | * @uses CONTEXT_COURSECAT | |
df73f8d4 | 1863 | * @return boolean Whether the user can create courses in any category in the system. |
49860445 | 1864 | */ |
1865 | function user_can_create_courses() { | |
c3df0901 | 1866 | global $DB; |
df73f8d4 | 1867 | $catsrs = $DB->get_recordset('course_categories'); |
4d55255b | 1868 | foreach ($catsrs as $cat) { |
b0c6dc1c | 1869 | if (has_capability('moodle/course:create', context_coursecat::instance($cat->id))) { |
df73f8d4 | 1870 | $catsrs->close(); |
1871 | return true; | |
49860445 | 1872 | } |
1873 | } | |
df73f8d4 | 1874 | $catsrs->close(); |
1875 | return false; | |
49860445 | 1876 | } |
c1857269 TH |
1877 | |
1878 | /** | |
1879 | * This method can update the values in mulitple database rows for a colum with | |
1880 | * a unique index, without violating that constraint. | |
1881 | * | |
1882 | * Suppose we have a table with a unique index on (otherid, sortorder), and | |
1883 | * for a particular value of otherid, we want to change all the sort orders. | |
1884 | * You have to do this carefully or you will violate the unique index at some time. | |
1885 | * This method takes care of the details for you. | |
1886 | * | |
1887 | * Note that, it is the responsibility of the caller to make sure that the | |
1888 | * requested rename is legal. For example, if you ask for [1 => 2, 2 => 2] | |
1889 | * then you will get a unique key violation error from the database. | |
1890 | * | |
1891 | * @param string $table The database table to modify. | |
1892 | * @param string $field the field that contains the values we are going to change. | |
1893 | * @param array $newvalues oldvalue => newvalue how to change the values. | |
1894 | * E.g. [1 => 4, 2 => 1, 3 => 3, 4 => 2]. | |
1895 | * @param array $otherconditions array fieldname => requestedvalue extra WHERE clause | |
1896 | * conditions to restrict which rows are affected. E.g. array('otherid' => 123). | |
1897 | * @param int $unusedvalue (defaults to -1) a value that is never used in $ordercol. | |
1898 | */ | |
1899 | function update_field_with_unique_index($table, $field, array $newvalues, | |
1900 | array $otherconditions, $unusedvalue = -1) { | |
1901 | global $DB; | |
1902 | $safechanges = decompose_update_into_safe_changes($newvalues, $unusedvalue); | |
1903 | ||
1904 | $transaction = $DB->start_delegated_transaction(); | |
1905 | foreach ($safechanges as $change) { | |
1906 | list($from, $to) = $change; | |
1907 | $otherconditions[$field] = $from; | |
1908 | $DB->set_field($table, $field, $to, $otherconditions); | |
1909 | } | |
1910 | $transaction->allow_commit(); | |
1911 | } | |
1912 | ||
1913 | /** | |
1914 | * Helper used by {@link update_field_with_unique_index()}. Given a desired | |
1915 | * set of changes, break them down into single udpates that can be done one at | |
1916 | * a time without breaking any unique index constraints. | |
1917 | * | |
1918 | * Suppose the input is array(1 => 2, 2 => 1) and -1. Then the output will be | |
1919 | * array (array(1, -1), array(2, 1), array(-1, 2)). This function solves this | |
1920 | * problem in the general case, not just for simple swaps. The unit tests give | |
1921 | * more examples. | |
1922 | * | |
1923 | * Note that, it is the responsibility of the caller to make sure that the | |
1924 | * requested rename is legal. For example, if you ask for something impossible | |
1925 | * like array(1 => 2, 2 => 2) then the results are undefined. (You will probably | |
1926 | * get a unique key violation error from the database later.) | |
1927 | * | |
1928 | * @param array $newvalues The desired re-ordering. | |
1929 | * E.g. array(1 => 4, 2 => 1, 3 => 3, 4 => 2). | |
1930 | * @param int $unusedvalue A value that is not currently used. | |
1931 | * @return array A safe way to perform the re-order. An array of two-element | |
1932 | * arrays array($from, $to). | |
1933 | * E.g. array(array(1, -1), array(2, 1), array(4, 2), array(-1, 4)). | |
1934 | */ | |
1935 | function decompose_update_into_safe_changes(array $newvalues, $unusedvalue) { | |
1936 | $nontrivialmap = array(); | |
1937 | foreach ($newvalues as $from => $to) { | |
1938 | if ($from == $unusedvalue || $to == $unusedvalue) { | |
1939 | throw new \coding_exception('Supposedly unused value ' . $unusedvalue . ' is actually used!'); | |
1940 | } | |
1941 | if ($from != $to) { | |
1942 | $nontrivialmap[$from] = $to; | |
1943 | } | |
1944 | } | |
1945 | ||
1946 | if (empty($nontrivialmap)) { | |
1947 | return array(); | |
1948 | } | |
1949 | ||
1950 | // First we deal with all renames that are not part of cycles. | |
1951 | // This bit is O(n^2) and it ought to be possible to do better, | |
1952 | // but it does not seem worth the effort. | |
1953 | $safechanges = array(); | |
1954 | $nontrivialmapchanged = true; | |
1955 | while ($nontrivialmapchanged) { | |
1956 | $nontrivialmapchanged = false; | |
1957 | ||
1958 | foreach ($nontrivialmap as $from => $to) { | |
1959 | if (array_key_exists($to, $nontrivialmap)) { | |
1960 | continue; // Cannot currenly do this rename. | |
1961 | } | |
1962 | // Is safe to do this rename now. | |
1963 | $safechanges[] = array($from, $to); | |
1964 | unset($nontrivialmap[$from]); | |
1965 | $nontrivialmapchanged = true; | |
1966 | } | |
1967 | } | |
1968 | ||
1969 | // Are we done? | |
1970 | if (empty($nontrivialmap)) { | |
1971 | return $safechanges; | |
1972 | } | |
1973 | ||
1974 | // Now what is left in $nontrivialmap must be a permutation, | |
1975 | // which must be a combination of disjoint cycles. We need to break them. | |
1976 | while (!empty($nontrivialmap)) { | |
1977 | // Extract the first cycle. | |
1978 | reset($nontrivialmap); | |
1979 | $current = $cyclestart = key($nontrivialmap); | |
1980 | $cycle = array(); | |
1981 | do { | |
1982 | $cycle[] = $current; | |
1983 | $next = $nontrivialmap[$current]; | |
1984 | unset($nontrivialmap[$current]); | |
1985 | $current = $next; | |
c35cf42e | 1986 | } while ($current != $cyclestart); |
c1857269 TH |
1987 | |
1988 | // Now convert it to a sequence of safe renames by using a temp. | |
1989 | $safechanges[] = array($cyclestart, $unusedvalue); | |
1990 | $cycle[0] = $unusedvalue; | |
1991 | $to = $cyclestart; | |
1992 | while ($from = array_pop($cycle)) { | |
1993 | $safechanges[] = array($from, $to); | |
1994 | $to = $from; | |
1995 | } | |
1996 | } | |
1997 | ||
1998 | return $safechanges; | |
1999 | } |