Moodle release 3.0rc3
[moodle.git] / lib / db / upgradelib.php
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/>.
17 /**
18  * Upgrade helper functions
19  *
20  * This file is used for special upgrade functions - for example groups and gradebook.
21  * These functions must use SQL and database related functions only- no other Moodle API,
22  * because it might depend on db structures that are not yet present during upgrade.
23  * (Do not use functions from accesslib.php, grades classes or group functions at all!)
24  *
25  * @package   core_install
26  * @category  upgrade
27  * @copyright 2007 Petr Skoda (http://skodak.org)
28  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
29  */
31 defined('MOODLE_INTERNAL') || die();
33 /**
34  * Returns all non-view and non-temp tables with sane names.
35  * Prints list of non-supported tables using $OUTPUT->notification()
36  *
37  * @return array
38  */
39 function upgrade_mysql_get_supported_tables() {
40     global $OUTPUT, $DB;
42     $tables = array();
43     $patprefix = str_replace('_', '\\_', $DB->get_prefix());
44     $pregprefix = preg_quote($DB->get_prefix(), '/');
46     $sql = "SHOW FULL TABLES LIKE '$patprefix%'";
47     $rs = $DB->get_recordset_sql($sql);
48     foreach ($rs as $record) {
49         $record = array_change_key_case((array)$record, CASE_LOWER);
50         $type = $record['table_type'];
51         unset($record['table_type']);
52         $fullname = array_shift($record);
54         if ($pregprefix === '') {
55             $name = $fullname;
56         } else {
57             $count = null;
58             $name = preg_replace("/^$pregprefix/", '', $fullname, -1, $count);
59             if ($count !== 1) {
60                 continue;
61             }
62         }
64         if (!preg_match("/^[a-z][a-z0-9_]*$/", $name)) {
65             echo $OUTPUT->notification("Database table with invalid name '$fullname' detected, skipping.", 'notifyproblem');
66             continue;
67         }
68         if ($type === 'VIEW') {
69             echo $OUTPUT->notification("Unsupported database table view '$fullname' detected, skipping.", 'notifyproblem');
70             continue;
71         }
72         $tables[$name] = $name;
73     }
74     $rs->close();
76     return $tables;
77 }
79 /**
80  * Remove all signed numbers from current database and change
81  * text fields to long texts - mysql only.
82  */
83 function upgrade_mysql_fix_unsigned_and_lob_columns() {
84     // We are not using standard API for changes of column
85     // because everything 'signed'-related will be removed soon.
87     // If anybody already has numbers higher than signed limit the execution stops
88     // and tables must be fixed manually before continuing upgrade.
90     global $DB;
92     if ($DB->get_dbfamily() !== 'mysql') {
93         return;
94     }
96     $pbar = new progress_bar('mysqlconvertunsignedlobs', 500, true);
98     $prefix = $DB->get_prefix();
99     $tables = upgrade_mysql_get_supported_tables();
101     $tablecount = count($tables);
102     $i = 0;
103     foreach ($tables as $table) {
104         $i++;
106         $changes = array();
108         $sql = "SHOW COLUMNS FROM `{{$table}}`";
109         $rs = $DB->get_recordset_sql($sql);
110         foreach ($rs as $column) {
111             $column = (object)array_change_key_case((array)$column, CASE_LOWER);
112             if (stripos($column->type, 'unsigned') !== false) {
113                 $maxvalue = 0;
114                 if (preg_match('/^int/i', $column->type)) {
115                     $maxvalue = 2147483647;
116                 } else if (preg_match('/^medium/i', $column->type)) {
117                     $maxvalue = 8388607;
118                 } else if (preg_match('/^smallint/i', $column->type)) {
119                     $maxvalue = 32767;
120                 } else if (preg_match('/^tinyint/i', $column->type)) {
121                     $maxvalue = 127;
122                 }
123                 if ($maxvalue) {
124                     // Make sure nobody is abusing our integer ranges - moodle int sizes are in digits, not bytes!!!
125                     $invalidcount = $DB->get_field_sql("SELECT COUNT('x') FROM `{{$table}}` WHERE `$column->field` > :maxnumber", array('maxnumber'=>$maxvalue));
126                     if ($invalidcount) {
127                         throw new moodle_exception('notlocalisederrormessage', 'error', new moodle_url('/admin/'), "Database table '{$table}'' contains unsigned column '{$column->field}' with $invalidcount values that are out of allowed range, upgrade can not continue.");
128                     }
129                 }
130                 $type = preg_replace('/unsigned/i', 'signed', $column->type);
131                 $notnull = ($column->null === 'NO') ? 'NOT NULL' : 'NULL';
132                 $default = (!is_null($column->default) and $column->default !== '') ? "DEFAULT '$column->default'" : '';
133                 $autoinc = (stripos($column->extra, 'auto_increment') !== false) ? 'AUTO_INCREMENT' : '';
134                 // Primary and unique not necessary here, change_database_structure does not add prefix.
135                 $changes[] = "MODIFY COLUMN `$column->field` $type $notnull $default $autoinc";
137             } else if ($column->type === 'tinytext' or $column->type === 'mediumtext' or $column->type === 'text') {
138                 $notnull = ($column->null === 'NO') ? 'NOT NULL' : 'NULL';
139                 $default = (!is_null($column->default) and $column->default !== '') ? "DEFAULT '$column->default'" : '';
140                 // Primary, unique and inc are not supported for texts.
141                 $changes[] = "MODIFY COLUMN `$column->field` LONGTEXT $notnull $default";
143             } else if ($column->type === 'tinyblob' or $column->type === 'mediumblob' or $column->type === 'blob') {
144                 $notnull = ($column->null === 'NO') ? 'NOT NULL' : 'NULL';
145                 $default = (!is_null($column->default) and $column->default !== '') ? "DEFAULT '$column->default'" : '';
146                 // Primary, unique and inc are not supported for blobs.
147                 $changes[] = "MODIFY COLUMN `$column->field` LONGBLOB $notnull $default";
148             }
150         }
151         $rs->close();
153         if ($changes) {
154             // Set appropriate timeout - 1 minute per thousand of records should be enough, min 60 minutes just in case.
155             $count = $DB->count_records($table, array());
156             $timeout = ($count/1000)*60;
157             $timeout = ($timeout < 60*60) ? 60*60 : (int)$timeout;
158             upgrade_set_timeout($timeout);
160             $sql = "ALTER TABLE `{$prefix}$table` ".implode(', ', $changes);
161             $DB->change_database_structure($sql);
162         }
164         $pbar->update($i, $tablecount, "Converted unsigned/lob columns in MySQL database - $i/$tablecount.");
165     }
168 /**
169  * Migrate NTEXT to NVARCHAR(MAX).
170  */
171 function upgrade_mssql_nvarcharmax() {
172     global $DB;
174     if ($DB->get_dbfamily() !== 'mssql') {
175         return;
176     }
178     $pbar = new progress_bar('mssqlconvertntext', 500, true);
180     $prefix = $DB->get_prefix();
181     $tables = $DB->get_tables(false);
183     $tablecount = count($tables);
184     $i = 0;
185     foreach ($tables as $table) {
186         $i++;
188         $columns = array();
190         $sql = "SELECT column_name
191                   FROM INFORMATION_SCHEMA.COLUMNS
192                  WHERE table_name = '{{$table}}' AND UPPER(data_type) = 'NTEXT'";
193         $rs = $DB->get_recordset_sql($sql);
194         foreach ($rs as $column) {
195             $columns[] = $column->column_name;
196         }
197         $rs->close();
199         if ($columns) {
200             // Set appropriate timeout - 1 minute per thousand of records should be enough, min 60 minutes just in case.
201             $count = $DB->count_records($table, array());
202             $timeout = ($count/1000)*60;
203             $timeout = ($timeout < 60*60) ? 60*60 : (int)$timeout;
204             upgrade_set_timeout($timeout);
206             $updates = array();
207             foreach ($columns as $column) {
208                 // Change the definition.
209                 $sql = "ALTER TABLE {$prefix}$table ALTER COLUMN $column NVARCHAR(MAX)";
210                 $DB->change_database_structure($sql);
211                 $updates[] = "$column = $column";
212             }
214             // Now force the migration of text data to new optimised storage.
215             $sql = "UPDATE {{$table}} SET ".implode(', ', $updates);
216             $DB->execute($sql);
217         }
219         $pbar->update($i, $tablecount, "Converted NTEXT to NVARCHAR(MAX) columns in MS SQL Server database - $i/$tablecount.");
220     }
223 /**
224  * Migrate IMAGE to VARBINARY(MAX).
225  */
226 function upgrade_mssql_varbinarymax() {
227     global $DB;
229     if ($DB->get_dbfamily() !== 'mssql') {
230         return;
231     }
233     $pbar = new progress_bar('mssqlconvertimage', 500, true);
235     $prefix = $DB->get_prefix();
236     $tables = $DB->get_tables(false);
238     $tablecount = count($tables);
239     $i = 0;
240     foreach ($tables as $table) {
241         $i++;
243         $columns = array();
245         $sql = "SELECT column_name
246                   FROM INFORMATION_SCHEMA.COLUMNS
247                  WHERE table_name = '{{$table}}' AND UPPER(data_type) = 'IMAGE'";
248         $rs = $DB->get_recordset_sql($sql);
249         foreach ($rs as $column) {
250             $columns[] = $column->column_name;
251         }
252         $rs->close();
254         if ($columns) {
255             // Set appropriate timeout - 1 minute per thousand of records should be enough, min 60 minutes just in case.
256             $count = $DB->count_records($table, array());
257             $timeout = ($count/1000)*60;
258             $timeout = ($timeout < 60*60) ? 60*60 : (int)$timeout;
259             upgrade_set_timeout($timeout);
261             foreach ($columns as $column) {
262                 // Change the definition.
263                 $sql = "ALTER TABLE {$prefix}$table ALTER COLUMN $column VARBINARY(MAX)";
264                 $DB->change_database_structure($sql);
265             }
267             // Binary columns should not be used, do not waste time optimising the storage.
268         }
270         $pbar->update($i, $tablecount, "Converted IMAGE to VARBINARY(MAX) columns in MS SQL Server database - $i/$tablecount.");
271     }
274 /**
275  * This upgrade script fixes the mismatches between DB fields course_modules.section
276  * and course_sections.sequence. It makes sure that each module is included
277  * in the sequence of at least one section.
278  * Note that this script is different from admin/cli/fix_course_sortorder.php
279  * in the following ways:
280  * 1. It does not fix the cases when module appears several times in section(s) sequence(s) -
281  *    it will be done automatically on the next viewing of the course.
282  * 2. It does not remove non-existing modules from section sequences - administrator
283  *    has to run the CLI script to do it.
284  * 3. When this script finds an orphaned module it adds it to the section but makes hidden
285  *    where CLI script does not change the visiblity specified in the course_modules table.
286  */
287 function upgrade_course_modules_sequences() {
288     global $DB;
290     // Find all modules that point to the section which does not point back to this module.
291     $sequenceconcat = $DB->sql_concat("','", "s.sequence", "','");
292     $moduleconcat = $DB->sql_concat("'%,'", "m.id", "',%'");
293     $sql = "SELECT m.id, m.course, m.section, s.sequence
294         FROM {course_modules} m LEFT OUTER JOIN {course_sections} s
295         ON m.course = s.course and m.section = s.id
296         WHERE s.sequence IS NULL OR ($sequenceconcat NOT LIKE $moduleconcat)
297         ORDER BY m.course";
298     $rs = $DB->get_recordset_sql($sql);
299     $sections = null;
300     foreach ($rs as $cm) {
301         if (!isset($sections[$cm->course])) {
302             // Retrieve all sections for the course (only once for each corrupt course).
303             $sections = array($cm->course =>
304                     $DB->get_records('course_sections', array('course' => $cm->course),
305                             'section', 'id, section, sequence, visible'));
306             if (empty($sections[$cm->course])) {
307                 // Very odd - the course has a module in it but has no sections. Create 0-section.
308                 $newsection = array('sequence' => '', 'section' => 0, 'visible' => 1);
309                 $newsection['id'] = $DB->insert_record('course_sections',
310                         $newsection + array('course' => $cm->course, 'summary' => '', 'summaryformat' => FORMAT_HTML));
311                 $sections[$cm->course] = array($newsection['id'] => (object)$newsection);
312             }
313         }
314         // Attempt to find the section that has this module in it's sequence.
315         // If there are several of them, pick the last because this is what get_fast_modinfo() does.
316         $sectionid = null;
317         foreach ($sections[$cm->course] as $section) {
318             if (!empty($section->sequence) && in_array($cm->id, preg_split('/,/', $section->sequence))) {
319                 $sectionid = $section->id;
320             }
321         }
322         if ($sectionid) {
323             // Found the section. Update course_module to point to the correct section.
324             $params = array('id' => $cm->id, 'section' => $sectionid);
325             if (!$sections[$cm->course][$sectionid]->visible) {
326                 $params['visible'] = 0;
327             }
328             $DB->update_record('course_modules', $params);
329         } else {
330             // No section in the course has this module in it's sequence.
331             if (isset($sections[$cm->course][$cm->section])) {
332                 // Try to add module to the section it points to (if it is valid).
333                 $sectionid = $cm->section;
334             } else {
335                 // Section not found. Just add to the first available section.
336                 reset($sections[$cm->course]);
337                 $sectionid = key($sections[$cm->course]);
338             }
339             $newsequence = ltrim($sections[$cm->course][$sectionid]->sequence . ',' . $cm->id, ',');
340             $sections[$cm->course][$sectionid]->sequence = $newsequence;
341             $DB->update_record('course_sections', array('id' => $sectionid, 'sequence' => $newsequence));
342             // Make module invisible because it was not displayed at all before this upgrade script.
343             $DB->update_record('course_modules', array('id' => $cm->id, 'section' => $sectionid, 'visible' => 0, 'visibleold' => 0));
344         }
345     }
346     $rs->close();
347     unset($sections);
349     // Note that we don't need to reset course cache here because it is reset automatically after upgrade.
352 /**
353  * Updates a single item (course module or course section) to transfer the
354  * availability settings from the old to the new format.
355  *
356  * Note: We do not convert groupmembersonly for modules at present. If we did,
357  * $groupmembersonly would be set to the groupmembersonly option for the
358  * module. Since we don't, it will be set to 0 for modules, and 1 for sections
359  * if they have a grouping.
360  *
361  * @param int $groupmembersonly 1 if activity has groupmembersonly option
362  * @param int $groupingid Grouping id (0 = none)
363  * @param int $availablefrom Available from time (0 = none)
364  * @param int $availableuntil Available until time (0 = none)
365  * @param int $showavailability Show availability (1) or hide activity entirely
366  * @param array $availrecs Records from course_modules/sections_availability
367  * @param array $fieldrecs Records from course_modules/sections_avail_fields
368  */
369 function upgrade_availability_item($groupmembersonly, $groupingid,
370         $availablefrom, $availableuntil, $showavailability,
371         array $availrecs, array $fieldrecs) {
372     global $CFG, $DB;
373     $conditions = array();
374     $shows = array();
376     // Group members only condition (if enabled).
377     if ($CFG->enablegroupmembersonly && $groupmembersonly) {
378         if ($groupingid) {
379             $conditions[] = '{"type":"grouping"' .
380                     ($groupingid ? ',"id":' . $groupingid : '') . '}';
381         } else {
382             // No grouping specified, so allow any group.
383             $conditions[] = '{"type":"group"}';
384         }
385         // Group members only condition was not displayed to students.
386         $shows[] = 'false';
388         // In the unlikely event that the site had enablegroupmembers only
389         // but NOT enableavailability, we need to turn this on now.
390         if (!$CFG->enableavailability) {
391             set_config('enableavailability', 1);
392         }
393     }
395     // Date conditions.
396     if ($availablefrom) {
397         $conditions[] = '{"type":"date","d":">=","t":' . $availablefrom . '}';
398         $shows[] = $showavailability ? 'true' : 'false';
399     }
400     if ($availableuntil) {
401         $conditions[] = '{"type":"date","d":"<","t":' . $availableuntil . '}';
402         // Until dates never showed to students.
403         $shows[] = 'false';
404     }
406     // Conditions from _availability table.
407     foreach ($availrecs as $rec) {
408         if (!empty($rec->sourcecmid)) {
409             // Completion condition.
410             $conditions[] = '{"type":"completion","cm":' . $rec->sourcecmid .
411                     ',"e":' . $rec->requiredcompletion . '}';
412         } else {
413             // Grade condition.
414             $minmax = '';
415             if (!empty($rec->grademin)) {
416                 $minmax .= ',"min":' . sprintf('%.5f', $rec->grademin);
417             }
418             if (!empty($rec->grademax)) {
419                 $minmax .= ',"max":' . sprintf('%.5f', $rec->grademax);
420             }
421             $conditions[] = '{"type":"grade","id":' . $rec->gradeitemid . $minmax . '}';
422         }
423         $shows[] = $showavailability ? 'true' : 'false';
424     }
426     // Conditions from _fields table.
427     foreach ($fieldrecs as $rec) {
428         if (isset($rec->userfield)) {
429             // Standard field.
430             $fieldbit = ',"sf":' . json_encode($rec->userfield);
431         } else {
432             // Custom field.
433             $fieldbit = ',"cf":' . json_encode($rec->shortname);
434         }
435         // Value is not included for certain operators.
436         switch($rec->operator) {
437             case 'isempty':
438             case 'isnotempty':
439                 $valuebit = '';
440                 break;
442             default:
443                 $valuebit = ',"v":' . json_encode($rec->value);
444                 break;
445         }
446         $conditions[] = '{"type":"profile","op":"' . $rec->operator . '"' .
447                 $fieldbit . $valuebit . '}';
448         $shows[] = $showavailability ? 'true' : 'false';
449     }
451     // If there are some conditions, set them into database.
452     if ($conditions) {
453         return '{"op":"&","showc":[' . implode(',', $shows) . '],' .
454                 '"c":[' . implode(',', $conditions) . ']}';
455     } else {
456         return null;
457     }
460 /**
461  * Using data for a single course-module that has groupmembersonly enabled,
462  * returns the new availability value that incorporates the correct
463  * groupmembersonly option.
464  *
465  * Included as a function so that it can be shared between upgrade and restore,
466  * and unit-tested.
467  *
468  * @param int $groupingid Grouping id for the course-module (0 if none)
469  * @param string $availability Availability JSON data for the module (null if none)
470  * @return string New value for availability for the module
471  */
472 function upgrade_group_members_only($groupingid, $availability) {
473     // Work out the new JSON object representing this option.
474     if ($groupingid) {
475         // Require specific grouping.
476         $condition = (object)array('type' => 'grouping', 'id' => (int)$groupingid);
477     } else {
478         // No grouping specified, so require membership of any group.
479         $condition = (object)array('type' => 'group');
480     }
482     if (is_null($availability)) {
483         // If there are no conditions using the new API then just set it.
484         $tree = (object)array('op' => '&', 'c' => array($condition), 'showc' => array(false));
485     } else {
486         // There are existing conditions.
487         $tree = json_decode($availability);
488         switch ($tree->op) {
489             case '&' :
490                 // For & conditions we can just add this one.
491                 $tree->c[] = $condition;
492                 $tree->showc[] = false;
493                 break;
494             case '!|' :
495                 // For 'not or' conditions we can add this one
496                 // but negated.
497                 $tree->c[] = (object)array('op' => '!&', 'c' => array($condition));
498                 $tree->showc[] = false;
499                 break;
500             default:
501                 // For the other two (OR and NOT AND) we have to add
502                 // an extra level to the tree.
503                 $tree = (object)array('op' => '&', 'c' => array($tree, $condition),
504                         'showc' => array($tree->show, false));
505                 // Inner trees do not have a show option, so remove it.
506                 unset($tree->c[0]->show);
507                 break;
508         }
509     }
511     return json_encode($tree);
514 /**
515  * Updates the mime-types for files that exist in the database, based on their
516  * file extension.
517  *
518  * @param array $filetypes Array with file extension as the key, and mimetype as the value
519  */
520 function upgrade_mimetypes($filetypes) {
521     global $DB;
522     $select = $DB->sql_like('filename', '?', false);
523     foreach ($filetypes as $extension=>$mimetype) {
524         $DB->set_field_select(
525             'files',
526             'mimetype',
527             $mimetype,
528             $select,
529             array($extension)
530         );
531     }
534 /**
535  * Marks all courses with changes in extra credit weight calculation
536  *
537  * Used during upgrade and in course restore process
538  *
539  * This upgrade script is needed because we changed the algorithm for calculating the automatic weights of extra
540  * credit items and want to prevent changes in the existing student grades.
541  *
542  * @param int $onlycourseid
543  */
544 function upgrade_extra_credit_weightoverride($onlycourseid = 0) {
545     global $DB;
547     // Find all courses that have categories in Natural aggregation method where there is at least one extra credit
548     // item and at least one item with overridden weight.
549     $courses = $DB->get_fieldset_sql(
550         "SELECT DISTINCT gc.courseid
551           FROM {grade_categories} gc
552           INNER JOIN {grade_items} gi ON gc.id = gi.categoryid AND gi.weightoverride = :weightoverriden
553           INNER JOIN {grade_items} gie ON gc.id = gie.categoryid AND gie.aggregationcoef = :extracredit
554           WHERE gc.aggregation = :naturalaggmethod" . ($onlycourseid ? " AND gc.courseid = :onlycourseid" : ''),
555         array('naturalaggmethod' => 13,
556             'weightoverriden' => 1,
557             'extracredit' => 1,
558             'onlycourseid' => $onlycourseid,
559         )
560     );
561     foreach ($courses as $courseid) {
562         $gradebookfreeze = get_config('core', 'gradebook_calculations_freeze_' . $courseid);
563         if (!$gradebookfreeze) {
564             set_config('gradebook_calculations_freeze_' . $courseid, 20150619);
565         }
566     }
569 /**
570  * Marks all courses that require calculated grade items be updated.
571  *
572  * Used during upgrade and in course restore process.
573  *
574  * This upgrade script is needed because the calculated grade items were stuck with a maximum of 100 and could be changed.
575  * This flags the courses that are affected and the grade book is frozen to retain grade integrity.
576  *
577  * @param int $courseid Specify a course ID to run this script on just one course.
578  */
579 function upgrade_calculated_grade_items($courseid = null) {
580     global $DB, $CFG;
582     $affectedcourses = array();
583     $possiblecourseids = array();
584     $params = array();
585     $singlecoursesql = '';
586     if (isset($courseid)) {
587         $singlecoursesql = "AND ns.id = :courseid";
588         $params['courseid'] = $courseid;
589     }
590     $siteminmaxtouse = 1;
591     if (isset($CFG->grade_minmaxtouse)) {
592         $siteminmaxtouse = $CFG->grade_minmaxtouse;
593     }
594     $courseidsql = "SELECT ns.id
595                       FROM (
596                         SELECT c.id, coalesce(" . $DB->sql_compare_text('gs.value') . ", :siteminmax) AS gradevalue
597                           FROM {course} c
598                           LEFT JOIN {grade_settings} gs
599                             ON c.id = gs.courseid
600                            AND ((gs.name = 'minmaxtouse' AND " . $DB->sql_compare_text('gs.value') . " = '2'))
601                         ) ns
602                     WHERE " . $DB->sql_compare_text('ns.gradevalue') . " = '2' $singlecoursesql";
603     $params['siteminmax'] = $siteminmaxtouse;
604     $courses = $DB->get_records_sql($courseidsql, $params);
605     foreach ($courses as $course) {
606         $possiblecourseids[$course->id] = $course->id;
607     }
609     if (!empty($possiblecourseids)) {
610         list($sql, $params) = $DB->get_in_or_equal($possiblecourseids);
611         // A calculated grade item grade min != 0 and grade max != 100 and the course setting is set to
612         // "Initial min and max grades".
613         $coursesql = "SELECT DISTINCT courseid
614                         FROM {grade_items}
615                        WHERE calculation IS NOT NULL
616                          AND itemtype = 'manual'
617                          AND (grademax <> 100 OR grademin <> 0)
618                          AND courseid $sql";
619         $affectedcourses = $DB->get_records_sql($coursesql, $params);
620     }
622     // Check for second type of affected courses.
623     // If we already have the courseid parameter set in the affectedcourses then there is no need to run through this section.
624     if (!isset($courseid) || !in_array($courseid, $affectedcourses)) {
625         $singlecoursesql = '';
626         $params = array();
627         if (isset($courseid)) {
628             $singlecoursesql = "AND courseid = :courseid";
629             $params['courseid'] = $courseid;
630         }
631         $nestedsql = "SELECT id
632                         FROM {grade_items}
633                        WHERE itemtype = 'category'
634                          AND calculation IS NOT NULL $singlecoursesql";
635         $calculatedgradecategories = $DB->get_records_sql($nestedsql, $params);
636         $categoryids = array();
637         foreach ($calculatedgradecategories as $key => $gradecategory) {
638             $categoryids[$key] = $gradecategory->id;
639         }
641         if (!empty($categoryids)) {
642             list($sql, $params) = $DB->get_in_or_equal($categoryids);
643             // A category with a calculation where the raw grade min and the raw grade max don't match the grade min and grade max
644             // for the category.
645             $coursesql = "SELECT DISTINCT gi.courseid
646                             FROM {grade_grades} gg, {grade_items} gi
647                            WHERE gi.id = gg.itemid
648                              AND (gg.rawgrademax <> gi.grademax OR gg.rawgrademin <> gi.grademin)
649                              AND gi.id $sql";
650             $additionalcourses = $DB->get_records_sql($coursesql, $params);
651             foreach ($additionalcourses as $key => $additionalcourse) {
652                 if (!array_key_exists($key, $affectedcourses)) {
653                     $affectedcourses[$key] = $additionalcourse;
654                 }
655             }
656         }
657     }
659     foreach ($affectedcourses as $affectedcourseid) {
660         if (isset($CFG->upgrade_calculatedgradeitemsonlyregrade) && !($courseid)) {
661             $DB->set_field('grade_items', 'needsupdate', 1, array('courseid' => $affectedcourseid->courseid));
662         } else {
663             // Check to see if the gradebook freeze is already in affect.
664             $gradebookfreeze = get_config('core', 'gradebook_calculations_freeze_' . $affectedcourseid->courseid);
665             if (!$gradebookfreeze) {
666                 set_config('gradebook_calculations_freeze_' . $affectedcourseid->courseid, 20150627);
667             }
668         }
669     }
672 /**
673  * This upgrade script merges all tag instances pointing to the same course tag
674  *
675  * User id is no longer used for those tag instances
676  */
677 function upgrade_course_tags() {
678     global $DB;
679     $sql = "SELECT min(ti.id)
680         FROM {tag_instance} ti
681         LEFT JOIN {tag_instance} tii on tii.itemtype = ? and tii.itemid = ti.itemid and tii.tiuserid = 0 and tii.tagid = ti.tagid
682         where ti.itemtype = ? and ti.tiuserid <> 0 AND tii.id is null
683         group by ti.tagid, ti.itemid";
684     $ids = $DB->get_fieldset_sql($sql, array('course', 'course'));
685     if ($ids) {
686         list($idsql, $idparams) = $DB->get_in_or_equal($ids);
687         $DB->execute('UPDATE {tag_instance} SET tiuserid = 0 WHERE id ' . $idsql, $idparams);
688     }
689     $DB->execute("DELETE FROM {tag_instance} WHERE itemtype = ? AND tiuserid <> 0", array('course'));