From c12f6b3191599788bc38db35a9c670e8119293bf Mon Sep 17 00:00:00 2001 From: Marina Glancy Date: Wed, 30 Oct 2013 11:58:55 +1100 Subject: [PATCH] MDL-38228 upgrade: Rewrite and re-apply script that fixes corrupt course modules to sequence relation --- lib/db/upgrade.php | 26 ++++++++ lib/db/upgradelib.php | 144 ++++++++++++++++++++++++++++++++++++++++++ version.php | 2 +- 3 files changed, 171 insertions(+), 1 deletion(-) diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index aaedc61a134..b6ccbe896ea 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -1603,6 +1603,8 @@ function xmldb_main_upgrade($oldversion) { } if ($oldversion < 2013021801.01) { + // This upgrade step is re-written under MDL-38228 (see below). + /* // Retrieve the list of course_sections as a recordset to save memory $coursesections = $DB->get_recordset('course_sections', null, 'course, id', 'id, course, sequence'); foreach ($coursesections as $coursesection) { @@ -1646,6 +1648,7 @@ function xmldb_main_upgrade($oldversion) { } } $coursesections->close(); + */ // Main savepoint reached. upgrade_main_savepoint(true, 2013021801.01); @@ -1696,6 +1699,9 @@ function xmldb_main_upgrade($oldversion) { // This is checking to see if the site has been running a specific version with a bug in it // because this upgrade step is slow and is only needed if the site has been running with the affected versions. if ($oldversion >= 2012062504.08 && $oldversion < 2012062504.13) { + // This upgrade step is re-written under MDL-38228 (see below). + + /* // Retrieve the list of course_sections as a recordset to save memory. // This is to fix a regression caused by MDL-37939. // In this case the upgrade step is fixing records where: @@ -1754,6 +1760,7 @@ function xmldb_main_upgrade($oldversion) { $coursesections->close(); // No savepoint needed for this change. + */ } if ($oldversion < 2013032200.01) { @@ -2241,5 +2248,24 @@ function xmldb_main_upgrade($oldversion) { upgrade_main_savepoint(true, 2013051402.10); } + if ($oldversion < 2013051402.12) { + // MDL-38228. Corrected course_modules upgrade script instead of 2013021801.01. + + // This upgrade script fixes the mismatches between DB fields course_modules.section + // and course_sections.sequence. It makes sure that each module is included + // in the sequence of only one section and that course_modules.section points back to it. + + // This script in included in each major version upgrade process so make sure we don't run it twice. + if (empty($CFG->movingmoduleupgradescriptwasrun)) { + upgrade_course_modules_sequences(); + + // To skip running the same script on the upgrade to the next major release. + set_config('movingmoduleupgradescriptwasrun', 1); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2013051402.12); + } + return true; } diff --git a/lib/db/upgradelib.php b/lib/db/upgradelib.php index 6edc5d8c827..b3b060ad41e 100644 --- a/lib/db/upgradelib.php +++ b/lib/db/upgradelib.php @@ -164,3 +164,147 @@ function upgrade_mysql_fix_unsigned_and_lob_columns() { $pbar->update($i, $tablecount, "Converted unsigned/lob columns in MySQL database - $i/$tablecount."); } } + +/** + * This upgrade script fixes the mismatches between DB fields course_modules.section + * and course_sections.sequence. It makes sure that each module is included + * in the sequence of only one section and that course_modules.section points back to it. + * + * Orphaned modules (modules that were not included in any section sequence in this course) + * will be added to their sections (or 0-section if their section is not found) and + * made invisible since they were not accessible at all before this upgrade script. + * + * Note that this script does not remove non-existing modules from section sequences since + * such operation would require much more time. + */ +function upgrade_course_modules_sequences() { + global $DB; + + $affectedcourses = array(); + // Step 1. Find all modules that point to the section which does not point back to this module. + $sequenceconcat = $DB->sql_concat("','", "s.sequence", "','"); + $moduleconcat = $DB->sql_concat("'%,'", "m.id", "',%'"); + $sql = "SELECT m.id, m.course, m.section, s.sequence + FROM {course_modules} m LEFT OUTER JOIN {course_sections} s + ON m.course = s.course and m.section = s.id + WHERE s.sequence IS NULL OR ($sequenceconcat NOT LIKE $moduleconcat) + ORDER BY m.course"; + $rs = $DB->get_recordset_sql($sql); + $sections = null; + foreach ($rs as $cm) { + if (!isset($sections[$cm->course])) { + // Retrieve all sections for the course (only once for each corrupt course). + $sections = array($cm->course => + $DB->get_records('course_sections', array('course' => $cm->course), + 'section', 'id, section, sequence, visible')); + if (empty($sections[$cm->course])) { + // Very odd - the course has a module in it but has no sections. Create 0-section. + $newsection = array('sequence' => '', 'section' => 0, 'visible' => 1); + $newsection['id'] = $DB->insert_record('course_sections', + $newsection + array('course' => $cm->course, 'summary' => '', 'summaryformat' => FORMAT_HTML)); + $sections[$cm->course] = array($newsection['id'] => (object)$newsection); + } + $affectedcourses[$cm->course] = true; + } + // Attempt to find the section that has this module in it's sequence. + // If there are several of them, pick the last because this is what get_fast_modinfo() does. + $sectionid = null; + foreach ($sections[$cm->course] as $section) { + if (!empty($section->sequence) && in_array($cm->id, preg_split('/,/', $section->sequence))) { + $sectionid = $section->id; + } + } + if ($sectionid) { + // Found the section. Update course_module to point to the correct section. + $params = array('id' => $cm->id, 'section' => $sectionid); + if (!$sections[$cm->course][$sectionid]->visible) { + $params['visible'] = 0; + } + $DB->update_record('course_modules', $params); + } else { + // No section in the course has this module in it's sequence. + if (isset($sections[$cm->course][$cm->section])) { + // Try to add module to the section it points to (if it is valid). + $sectionid = $cm->section; + } else { + // Section not found. Just add to the first available section. + reset($sections[$cm->course]); + $sectionid = key($sections[$cm->course]); + } + $newsequence = ltrim($sections[$cm->course][$sectionid]->sequence . ',' . $cm->id, ','); + $sections[$cm->course][$sectionid]->sequence = $newsequence; + $DB->update_record('course_sections', array('id' => $sectionid, 'sequence' => $newsequence)); + // Make module invisible because it was not displayed at all before this upgrade script. + $DB->update_record('course_modules', array('id' => $cm->id, 'section' => $sectionid, 'visible' => 0, 'visibleold' => 0)); + } + } + $rs->close(); + unset($sections); + + // Step 2. Find all modules that are listed in sequence of another section or listed in sequence of their section twice. + $sequenceconcat = $DB->sql_concat("','", "s.sequence", "','"); + $moduleconcat = $DB->sql_concat("'%,'", "m.id", "',%'"); + $moduleconcatdup1 = $DB->sql_concat("'%,'", "m.id", "','", "m.id", "',%'"); + $moduleconcatdup2 = $DB->sql_concat("'%,'", "m.id", "',%,'", "m.id", "',%'"); + $sql = "SELECT m.id, m.course, m.section AS modulesectionid, + s.id AS sectionid, s.sequence AS sectionsequence, s.section AS sectionsectionnum, + ms.section AS modulesectionnum, ms.sequence AS modulesectionsequence + FROM {course_modules} m JOIN {course_sections} s + ON m.course = s.course AND + ( + (m.section <> s.id AND $sequenceconcat LIKE $moduleconcat) + OR + (m.section = s.id AND $sequenceconcat LIKE $moduleconcatdup1) + OR + (m.section = s.id AND $sequenceconcat LIKE $moduleconcatdup2) + ) + JOIN {course_sections} ms ON ms.id = m.section + ORDER BY m.course, m.id, m.section DESC"; + $rs = $DB->get_recordset_sql($sql); + $updatedsequences = array(); + $correctmodulesections = array(); + foreach ($rs as $cm) { + $incorrectsectionid = $cm->sectionid; + $incorrectsequence = $cm->sectionsequence; + if (!isset($correctmodulesections[$cm->id])) { + // Function get_fast_modinfo() believes that the section with the biggest sectionnum is the correct one. + // Let's correct everything else to match with how course is displayed to the students. + if ($cm->modulesectionnum > $cm->sectionsectionnum) { + $correctmodulesections[$cm->id] = $cm->modulesectionid; + } else { + $correctmodulesections[$cm->id] = $cm->sectionid; + // oops our module points to the wrong section. + $DB->update_record('course_modules', array('id' => $cm->id, + 'section' => $correctmodulesections[$cm->id])); + $incorrectsectionid = $cm->modulesectionid; + $incorrectsequence = $cm->modulesectionsequence; + } + } + if (isset($updatedsequences[$incorrectsectionid])) { + $sequence = $updatedsequences[$incorrectsectionid]; + } else { + $sequence = preg_split('/,/', $incorrectsequence); + } + if ($correctmodulesections[$cm->id] <> $incorrectsectionid) { + // Remove all occurences of module id from section sequence. + $sequence = array_diff($sequence, array($cm->id)); + } else { + // Remove all occurences of module id from section sequence except for the first one. + if (($idx = array_search($cm->id, $sequence)) !== false) { + $firstchunk = array_splice($sequence, 0, $idx+1); + $sequence = array_merge($firstchunk, array_diff($sequence, array($cm->id))); + } + } + $updatedsequences[$incorrectsectionid] = array_values($sequence); + $DB->update_record('course_sections', array('id' => $incorrectsectionid, + 'sequence' => join(',', $sequence))); + $affectedcourses[$cm->course] = true; + } + $rs->close(); + + // Reset course cache for affected courses. + if (!empty($affectedcourses)) { + list($sql, $params) = $DB->get_in_or_equal(array_keys($affectedcourses)); + $DB->execute("UPDATE {course} SET modinfo = null WHERE id ".$sql, $params); + } +} diff --git a/version.php b/version.php index 7e79c8d6a7a..a6dad768611 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2013051402.11; // 20130514 = branching date YYYYMMDD - do not modify! +$version = 2013051402.12; // 20130514 = branching date YYYYMMDD - do not modify! // RR = release increments - 00 in DEV branches // .XX = incremental changes