diff --git a/availability/condition/completion/tests/behat/availability_completion.feature b/availability/condition/completion/tests/behat/availability_completion.feature
index 0f4c121d968..2ac19496286 100644
--- a/availability/condition/completion/tests/behat/availability_completion.feature
+++ b/availability/condition/completion/tests/behat/availability_completion.feature
@@ -54,3 +54,58 @@ Feature: availability_completion
# Mark page 1 complete
When I toggle the manual completion state of "Page 1"
Then I should see "Page 2" in the "region-main" "region"
+
+ @javascript
+ Scenario: Test completion and course cache rebuild
+ Given the following "activities" exist:
+ | activity | name | intro | course | idnumber |
+ | forum | forum 1 | forum 1 | C1 | forum1 |
+ And I log in as "teacher1"
+ And I am on "Course 1" course homepage with editing mode on
+ And I open "forum 1" actions menu
+ And I click on "Edit settings" "link" in the "forum 1" activity
+ And I set the following fields to these values:
+ | Completion tracking | Show activity as complete when conditions are met |
+ | completionview | 1 |
+ | completionpostsenabled | 1 |
+ | completionposts | 2 |
+ And I press "Save and return to course"
+ And I add a new discussion to "forum 1" forum with:
+ | Subject | Forum post 1 |
+ | Message | This is the body |
+ And I am on "Course 1" course homepage with editing mode on
+ And I add a "Page" to section "2"
+ And I set the following fields to these values:
+ | Name | Page 2 |
+ | Description | Test |
+ | Page content | Test |
+ And I expand all fieldsets
+ And I press "Add restriction..."
+ And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
+ And I click on ".availability-item .availability-eye img" "css_element"
+ And I set the following fields to these values:
+ | Required completion status | must be marked complete |
+ | cm | forum 1 |
+ And I press "Save and return to course"
+ And I log out
+ And I log in as "student1"
+ When I am on "Course 1" course homepage
+ # Page 2 should not appear yet.
+ Then I should not see "Page 2" in the "region-main" "region"
+ And I click on "forum 1" "link" in the "region-main" "region"
+ # Page 2 should not appear yet.
+ And I should not see "Page 2" in the "region-main" "region"
+ And I log out
+ And I log in as "teacher1"
+ And I am on "Course 1" course homepage
+ And I am on the "forum 1" "forum activity editing" page
+ And I expand all fieldsets
+ And I set the following fields to these values:
+ | completionpostsenabled | 0 |
+ And I press "Save and display"
+ And I log out
+ And I log in as "student1"
+ And I am on "Course 1" course homepage
+ And I click on "forum 1" "link" in the "region-main" "region"
+ And I am on "Course 1" course homepage
+ And I should see "Page 2" in the "region-main" "region"
diff --git a/availability/condition/date/classes/condition.php b/availability/condition/date/classes/condition.php
index 63b61b4ebce..f7422ebbf74 100644
--- a/availability/condition/date/classes/condition.php
+++ b/availability/condition/date/classes/condition.php
@@ -292,14 +292,16 @@ class condition extends \core_availability\condition {
$updatesection->availability = json_encode($tree->save());
$updatesection->timemodified = time();
$DB->update_record('course_sections', $updatesection);
+ // Invalidate the section cache by given section id.
+ \course_modinfo::purge_course_section_cache_by_id($courseid, $section->id);
$anychanged = true;
}
}
- // Ensure course cache is cleared if required.
if ($anychanged) {
- rebuild_course_cache($courseid, true);
+ // Partial rebuild the sections which have been invalidated.
+ rebuild_course_cache($courseid, true, true);
}
}
}
diff --git a/course/dnduploadlib.php b/course/dnduploadlib.php
index cf23b5da001..d3be854817a 100644
--- a/course/dnduploadlib.php
+++ b/course/dnduploadlib.php
@@ -614,8 +614,10 @@ class dndupload_ajax_processor {
$visible = get_fast_modinfo($this->course)->get_section_info($this->section)->visible;
$DB->set_field('course_modules', 'instance', $instanceid, array('id' => $this->cm->id));
+
+ \course_modinfo::purge_course_module_cache($this->course->id, $this->cm->id);
// Rebuild the course cache after update action
- rebuild_course_cache($this->course->id, true);
+ rebuild_course_cache($this->course->id, true, true);
$sectionid = course_add_cm_to_section($this->course, $this->cm->id, $this->section);
diff --git a/course/format/classes/base.php b/course/format/classes/base.php
index a88695341fd..c75c95946e2 100644
--- a/course/format/classes/base.php
+++ b/course/format/classes/base.php
@@ -1095,7 +1095,15 @@ abstract class base {
}
}
if ($needrebuild) {
- rebuild_course_cache($this->courseid, true);
+ if ($sectionid) {
+ // Invalidate the section cache by given section id.
+ course_modinfo::purge_course_section_cache_by_id($this->courseid, $sectionid);
+ // Partial rebuild sections that have been invalidated.
+ rebuild_course_cache($this->courseid, true, true);
+ } else {
+ // Full rebuild if sectionid is null.
+ rebuild_course_cache($this->courseid);
+ }
}
if ($changed) {
// Reset internal caches.
@@ -1422,7 +1430,10 @@ abstract class base {
// Delete section and it's format options.
$DB->delete_records('course_format_options', array('sectionid' => $section->id));
$DB->delete_records('course_sections', array('id' => $section->id));
- rebuild_course_cache($course->id, true);
+ // Invalidate the section cache by given section id.
+ course_modinfo::purge_course_section_cache_by_id($course->id, $section->id);
+ // Partial rebuild section cache that has been purged.
+ rebuild_course_cache($course->id, true, true);
// Delete section summary files.
$context = \context_course::instance($course->id);
diff --git a/course/lib.php b/course/lib.php
index badbaf67fe9..46668cecd28 100644
--- a/course/lib.php
+++ b/course/lib.php
@@ -386,179 +386,6 @@ function course_integrity_check($courseid, $rawmods = null, $sections = null, $f
return $messages;
}
-/**
- * For a given course, returns an array of course activity objects
- * Each item in the array contains he following properties:
- */
-function get_array_of_activities($courseid) {
-// cm - course module id
-// mod - name of the module (eg forum)
-// section - the number of the section (eg week or topic)
-// name - the name of the instance
-// visible - is the instance visible or not
-// groupingid - grouping id
-// extra - contains extra string to include in any link
- global $CFG, $DB;
-
- $course = $DB->get_record('course', array('id'=>$courseid));
-
- if (empty($course)) {
- throw new moodle_exception('courseidnotfound');
- }
-
- $mod = array();
-
- $rawmods = get_course_mods($courseid);
- if (empty($rawmods)) {
- return $mod; // always return array
- }
- $courseformat = course_get_format($course);
-
- if ($sections = $DB->get_records('course_sections', array('course' => $courseid),
- 'section ASC', 'id,section,sequence,visible')) {
- // First check and correct obvious mismatches between course_sections.sequence and course_modules.section.
- if ($errormessages = course_integrity_check($courseid, $rawmods, $sections)) {
- debugging(join('
', $errormessages));
- $rawmods = get_course_mods($courseid);
- $sections = $DB->get_records('course_sections', array('course' => $courseid),
- 'section ASC', 'id,section,sequence,visible');
- }
- // Build array of activities.
- foreach ($sections as $section) {
- if (!empty($section->sequence)) {
- $sequence = explode(",", $section->sequence);
- foreach ($sequence as $seq) {
- if (empty($rawmods[$seq])) {
- continue;
- }
- // Adjust visibleoncoursepage, value in DB may not respect format availability.
- $rawmods[$seq]->visibleoncoursepage = (!$rawmods[$seq]->visible
- || $rawmods[$seq]->visibleoncoursepage
- || empty($CFG->allowstealth)
- || !$courseformat->allow_stealth_module_visibility($rawmods[$seq], $section)) ? 1 : 0;
-
- // Create an object that will be cached.
- $mod[$seq] = new stdClass();
- $mod[$seq]->id = $rawmods[$seq]->instance;
- $mod[$seq]->cm = $rawmods[$seq]->id;
- $mod[$seq]->mod = $rawmods[$seq]->modname;
-
- // Oh dear. Inconsistent names left here for backward compatibility.
- $mod[$seq]->section = $section->section;
- $mod[$seq]->sectionid = $rawmods[$seq]->section;
-
- $mod[$seq]->module = $rawmods[$seq]->module;
- $mod[$seq]->added = $rawmods[$seq]->added;
- $mod[$seq]->score = $rawmods[$seq]->score;
- $mod[$seq]->idnumber = $rawmods[$seq]->idnumber;
- $mod[$seq]->visible = $rawmods[$seq]->visible;
- $mod[$seq]->visibleoncoursepage = $rawmods[$seq]->visibleoncoursepage;
- $mod[$seq]->visibleold = $rawmods[$seq]->visibleold;
- $mod[$seq]->groupmode = $rawmods[$seq]->groupmode;
- $mod[$seq]->groupingid = $rawmods[$seq]->groupingid;
- $mod[$seq]->indent = $rawmods[$seq]->indent;
- $mod[$seq]->completion = $rawmods[$seq]->completion;
- $mod[$seq]->extra = "";
- $mod[$seq]->completiongradeitemnumber =
- $rawmods[$seq]->completiongradeitemnumber;
- $mod[$seq]->completionpassgrade = $rawmods[$seq]->completionpassgrade;
- $mod[$seq]->completionview = $rawmods[$seq]->completionview;
- $mod[$seq]->completionexpected = $rawmods[$seq]->completionexpected;
- $mod[$seq]->showdescription = $rawmods[$seq]->showdescription;
- $mod[$seq]->availability = $rawmods[$seq]->availability;
- $mod[$seq]->deletioninprogress = $rawmods[$seq]->deletioninprogress;
- $mod[$seq]->downloadcontent = $rawmods[$seq]->downloadcontent;
- $modname = $mod[$seq]->mod;
- $functionname = $modname."_get_coursemodule_info";
-
- if (!file_exists("$CFG->dirroot/mod/$modname/lib.php")) {
- continue;
- }
-
- include_once("$CFG->dirroot/mod/$modname/lib.php");
-
- if ($hasfunction = function_exists($functionname)) {
- if ($info = $functionname($rawmods[$seq])) {
- if (!empty($info->icon)) {
- $mod[$seq]->icon = $info->icon;
- }
- if (!empty($info->iconcomponent)) {
- $mod[$seq]->iconcomponent = $info->iconcomponent;
- }
- if (!empty($info->name)) {
- $mod[$seq]->name = $info->name;
- }
- if ($info instanceof cached_cm_info) {
- // When using cached_cm_info you can include three new fields
- // that aren't available for legacy code
- if (!empty($info->content)) {
- $mod[$seq]->content = $info->content;
- }
- if (!empty($info->extraclasses)) {
- $mod[$seq]->extraclasses = $info->extraclasses;
- }
- if (!empty($info->iconurl)) {
- // Convert URL to string as it's easier to store. Also serialized object contains \0 byte and can not be written to Postgres DB.
- $url = new moodle_url($info->iconurl);
- $mod[$seq]->iconurl = $url->out(false);
- }
- if (!empty($info->onclick)) {
- $mod[$seq]->onclick = $info->onclick;
- }
- if (!empty($info->customdata)) {
- $mod[$seq]->customdata = $info->customdata;
- }
- } else {
- // When using a stdclass, the (horrible) deprecated ->extra field
- // is available for BC
- if (!empty($info->extra)) {
- $mod[$seq]->extra = $info->extra;
- }
- }
- }
- }
- // When there is no modname_get_coursemodule_info function,
- // but showdescriptions is enabled, then we use the 'intro'
- // and 'introformat' fields in the module table
- if (!$hasfunction && $rawmods[$seq]->showdescription) {
- if ($modvalues = $DB->get_record($rawmods[$seq]->modname,
- array('id' => $rawmods[$seq]->instance), 'name, intro, introformat')) {
- // Set content from intro and introformat. Filters are disabled
- // because we filter it with format_text at display time
- $mod[$seq]->content = format_module_intro($rawmods[$seq]->modname,
- $modvalues, $rawmods[$seq]->id, false);
-
- // To save making another query just below, put name in here
- $mod[$seq]->name = $modvalues->name;
- }
- }
- if (!isset($mod[$seq]->name)) {
- $mod[$seq]->name = $DB->get_field($rawmods[$seq]->modname, "name", array("id"=>$rawmods[$seq]->instance));
- }
-
- // Minimise the database size by unsetting default options when they are
- // 'empty'. This list corresponds to code in the cm_info constructor.
- foreach (array('idnumber', 'groupmode', 'groupingid',
- 'indent', 'completion', 'extra', 'extraclasses', 'iconurl', 'onclick', 'content',
- 'icon', 'iconcomponent', 'customdata', 'availability', 'completionview',
- 'completionexpected', 'score', 'showdescription', 'deletioninprogress') as $property) {
- if (property_exists($mod[$seq], $property) &&
- empty($mod[$seq]->{$property})) {
- unset($mod[$seq]->{$property});
- }
- }
- // Special case: this value is usually set to null, but may be 0
- if (property_exists($mod[$seq], 'completiongradeitemnumber') &&
- is_null($mod[$seq]->completiongradeitemnumber)) {
- unset($mod[$seq]->completiongradeitemnumber);
- }
- }
- }
- }
- }
- return $mod;
-}
-
/**
* Returns an array where the key is the module name (component name without 'mod_')
* and the value is a lang_string object with a human-readable string.
@@ -818,11 +645,7 @@ function course_add_cm_to_section($courseorid, $cmid, $sectionnum, $beforemod =
}
$DB->set_field("course_sections", "sequence", $newsequence, array("id" => $section->id));
$DB->set_field('course_modules', 'section', $section->id, array('id' => $cmid));
- if (is_object($courseorid)) {
- rebuild_course_cache($courseorid->id, true);
- } else {
- rebuild_course_cache($courseorid, true);
- }
+ rebuild_course_cache($courseid, true);
return $section->id; // Return course_sections ID that was used.
}
@@ -841,7 +664,8 @@ function set_coursemodule_groupmode($id, $groupmode) {
$cm = $DB->get_record('course_modules', array('id' => $id), 'id,course,groupmode', MUST_EXIST);
if ($cm->groupmode != $groupmode) {
$DB->set_field('course_modules', 'groupmode', $groupmode, array('id' => $cm->id));
- rebuild_course_cache($cm->course, true);
+ \course_modinfo::purge_course_module_cache($cm->course, $cm->id);
+ rebuild_course_cache($cm->course, false, true);
}
return ($cm->groupmode != $groupmode);
}
@@ -851,7 +675,8 @@ function set_coursemodule_idnumber($id, $idnumber) {
$cm = $DB->get_record('course_modules', array('id' => $id), 'id,course,idnumber', MUST_EXIST);
if ($cm->idnumber != $idnumber) {
$DB->set_field('course_modules', 'idnumber', $idnumber, array('id' => $cm->id));
- rebuild_course_cache($cm->course, true);
+ \course_modinfo::purge_course_module_cache($cm->course, $cm->id);
+ rebuild_course_cache($cm->course, false, true);
}
return ($cm->idnumber != $idnumber);
}
@@ -945,7 +770,8 @@ function set_coursemodule_visible($id, $visible, $visibleoncoursepage = 1) {
}
}
- rebuild_course_cache($cm->course, true);
+ \course_modinfo::purge_course_module_cache($cm->course, $cm->id);
+ rebuild_course_cache($cm->course, false, true);
return true;
}
@@ -982,7 +808,8 @@ function set_coursemodule_name($id, $name) {
$DB->update_record($cm->modname, $module);
$cm->name = $module->name;
\core\event\course_module_updated::create_from_cm($cm)->trigger();
- rebuild_course_cache($cm->course, true);
+ \course_modinfo::purge_course_module_cache($cm->course, $cm->id);
+ rebuild_course_cache($cm->course, false, true);
// Attempt to update the grade item if relevant.
$grademodule = $DB->get_record($cm->modname, array('id' => $cm->instance));
@@ -1134,13 +961,14 @@ function course_delete_module($cmid, $async = false) {
'context' => $modcontext,
'objectid' => $cm->id,
'other' => array(
- 'modulename' => $modulename,
+ 'modulename' => $modulename,
'instanceid' => $cm->instance,
)
));
$event->add_record_snapshot('course_modules', $cm);
$event->trigger();
- rebuild_course_cache($cm->course, true);
+ \course_modinfo::purge_course_module_cache($cm->course, $cm->id);
+ rebuild_course_cache($cm->course, false, true);
}
/**
@@ -1401,13 +1229,17 @@ function move_section_to($course, $section, $destination, $ignorenumsections = f
// uniqueness constraint
$transaction = $DB->start_delegated_transaction();
foreach ($movedsections as $id => $position) {
- if ($sections[$id] !== $position) {
- $DB->set_field('course_sections', 'section', -$position, array('id' => $id));
+ if ((int) $sections[$id] !== $position) {
+ $DB->set_field('course_sections', 'section', -$position, ['id' => $id]);
+ // Invalidate the section cache by given section id.
+ course_modinfo::purge_course_section_cache_by_id($course->id, $id);
}
}
foreach ($movedsections as $id => $position) {
- if ($sections[$id] !== $position) {
- $DB->set_field('course_sections', 'section', $position, array('id' => $id));
+ if ((int) $sections[$id] !== $position) {
+ $DB->set_field('course_sections', 'section', $position, ['id' => $id]);
+ // Invalidate the section cache by given section id.
+ course_modinfo::purge_course_section_cache_by_id($course->id, $id);
}
}
@@ -1415,14 +1247,14 @@ function move_section_to($course, $section, $destination, $ignorenumsections = f
// Adjust the higlighted section location if we move something over it either direction.
if ($section == $course->marker) {
course_set_marker($course->id, $destination);
- } elseif ($section > $course->marker && $course->marker >= $destination) {
+ } else if ($section > $course->marker && $course->marker >= $destination) {
course_set_marker($course->id, $course->marker+1);
- } elseif ($section < $course->marker && $course->marker <= $destination) {
+ } else if ($section < $course->marker && $course->marker <= $destination) {
course_set_marker($course->id, $course->marker-1);
}
$transaction->allow_commit();
- rebuild_course_cache($course->id, true);
+ rebuild_course_cache($course->id, true, true);
return true;
}
@@ -1597,7 +1429,9 @@ function course_update_section($course, $section, $data) {
$data['id'] = $section->id;
$data['timemodified'] = time();
$DB->update_record('course_sections', $data);
- rebuild_course_cache($courseid, true);
+ // Invalidate the section cache by given section id.
+ course_modinfo::purge_course_section_cache_by_id($courseid, $section->id);
+ rebuild_course_cache($courseid, false, true);
course_get_format($courseid)->update_section_format_options($data);
// Update fields of the $section object.
@@ -1629,11 +1463,13 @@ function course_update_section($course, $section, $data) {
} else {
// We hide the section, so we hide the module but we store the original state in visibleold.
set_coursemodule_visible($moduleid, 0, $cm->visibleoncoursepage);
- $DB->set_field('course_modules', 'visibleold', $cm->visible, array('id' => $moduleid));
+ $DB->set_field('course_modules', 'visibleold', $cm->visible, ['id' => $moduleid]);
+ \course_modinfo::purge_course_module_cache($cm->course, $cm->id);
}
\core\event\course_module_updated::create_from_cm($cm)->trigger();
}
}
+ rebuild_course_cache($courseid, false, true);
}
}
diff --git a/course/mod.php b/course/mod.php
index 67719a7aae2..a6c33395a6b 100644
--- a/course/mod.php
+++ b/course/mod.php
@@ -190,7 +190,9 @@ if ((!empty($movetosection) or !empty($moveto)) and confirm_sesskey()) {
$DB->set_field('course_modules', 'indent', $cm->indent, array('id'=>$cm->id));
- rebuild_course_cache($cm->course);
+ \course_modinfo::purge_course_module_cache($cm->course, $cm->id);
+ // Rebuild invalidated module cache.
+ rebuild_course_cache($cm->course, false, true);
redirect(course_get_url($course, $cm->sectionnum, array('sr' => $sectionreturn)));
diff --git a/course/modlib.php b/course/modlib.php
index c2b3bd8c8eb..26170a83785 100644
--- a/course/modlib.php
+++ b/course/modlib.php
@@ -377,7 +377,8 @@ function edit_module_post_actions($moduleinfo, $course) {
$moduleinfo->showgradingmanagement = $showgradingmanagement;
}
- rebuild_course_cache($course->id, true);
+ \course_modinfo::purge_course_module_cache($course->id, $moduleinfo->coursemodule);
+ rebuild_course_cache($course->id, true, true);
if ($hasgrades) {
grade_regrade_final_grades($course->id);
}
diff --git a/course/tests/courselib_test.php b/course/tests/courselib_test.php
index 3a42db3fa6f..f9c63c5aa02 100644
--- a/course/tests/courselib_test.php
+++ b/course/tests/courselib_test.php
@@ -1044,6 +1044,80 @@ class courselib_test extends advanced_testcase {
$this->assertEquals(3, $course->marker);
}
+ /**
+ * Test move_section_to method with caching
+ *
+ * @covers ::move_section_to
+ * @return void
+ */
+ public function test_move_section_with_section_cache(): void {
+ $this->resetAfterTest();
+ $this->setAdminUser();
+ $cache = cache::make('core', 'coursemodinfo');
+
+ // Generate the course and pre-requisite module.
+ $course = $this->getDataGenerator()->create_course(['format' => 'topics', 'numsections' => 3], ['createsections' => true]);
+ // Reset course cache.
+ rebuild_course_cache($course->id, true);
+
+ // Build course cache.
+ get_fast_modinfo($course->id);
+ // Get the course modinfo cache.
+ $coursemodinfo = $cache->get_versioned($course->id, $course->cacherev);
+ // Get the section cache.
+ $sectioncaches = $coursemodinfo->sectioncache;
+
+ // Make sure that we will have 4 section caches here.
+ $this->assertCount(4, $sectioncaches);
+ $this->assertArrayHasKey(0, $sectioncaches);
+ $this->assertArrayHasKey(1, $sectioncaches);
+ $this->assertArrayHasKey(2, $sectioncaches);
+ $this->assertArrayHasKey(3, $sectioncaches);
+
+ // Move section.
+ move_section_to($course, 2, 3);
+ // Get the course modinfo cache.
+ $coursemodinfo = $cache->get_versioned($course->id, $course->cacherev);
+ // Get the section cache.
+ $sectioncaches = $coursemodinfo->sectioncache;
+
+ // Make sure that we will have 2 section caches left.
+ $this->assertCount(2, $sectioncaches);
+ $this->assertArrayHasKey(0, $sectioncaches);
+ $this->assertArrayHasKey(1, $sectioncaches);
+ $this->assertArrayNotHasKey(2, $sectioncaches);
+ $this->assertArrayNotHasKey(3, $sectioncaches);
+ }
+
+ /**
+ * Test move_section_to method.
+ * Make sure that we only update the moving sections, not all the sections in the current course.
+ *
+ * @covers ::move_section_to
+ * @return void
+ */
+ public function test_move_section_to(): void {
+ global $DB, $CFG;
+ $this->resetAfterTest();
+ $this->setAdminUser();
+
+ // Generate the course and pre-requisite module.
+ $course = $this->getDataGenerator()->create_course(['format' => 'topics', 'numsections' => 3], ['createsections' => true]);
+
+ ob_start();
+ $DB->set_debug(true);
+ // Move section.
+ move_section_to($course, 2, 3);
+ $DB->set_debug(false);
+ $debuginfo = ob_get_contents();
+ ob_end_clean();
+ $sectionmovequerycount = substr_count($debuginfo, 'UPDATE ' . $CFG->phpunit_prefix . 'course_sections SET');
+ // We are updating the course_section table in steps to avoid breaking database uniqueness constraint.
+ // So the queries will be doubled. See: course/lib.php:1423
+ // Make sure that we only need 4 queries to update the position of section 2 and section 3.
+ $this->assertEquals(4, $sectionmovequerycount);
+ }
+
public function test_course_can_delete_section() {
global $DB;
$this->resetAfterTest(true);
@@ -1143,6 +1217,7 @@ class courselib_test extends advanced_testcase {
// Delete section in the middle (2).
$this->assertFalse(course_delete_section($course, 2, false));
+ $this->assertEquals(4, course_get_format($course)->get_last_section_number());
$this->assertTrue(course_delete_section($course, 2, true));
$this->assertFalse($DB->record_exists('course_modules', array('id' => $assign21->cmid)));
$this->assertFalse($DB->record_exists('course_modules', array('id' => $assign22->cmid)));
diff --git a/course/tests/externallib_test.php b/course/tests/externallib_test.php
index 60ba1a207b4..2dc265d3c68 100644
--- a/course/tests/externallib_test.php
+++ b/course/tests/externallib_test.php
@@ -1085,8 +1085,10 @@ class externallib_test extends externallib_advanced_testcase {
array('course' => $course->id, 'intro' => 'forum completion tracking auto', 'trackingtype' => 2),
array('showdescription' => true, 'completionview' => 1, 'completion' => COMPLETION_TRACKING_AUTOMATIC));
$forumcompleteautocm = get_coursemodule_from_id('forum', $forumcompleteauto->cmid);
-
- rebuild_course_cache($course->id, true);
+ $sectionrecord = $DB->get_record('course_sections', $conditions);
+ // Invalidate the section cache by given section number.
+ course_modinfo::purge_course_section_cache_by_number($sectionrecord->course, $sectionrecord->section);
+ rebuild_course_cache($course->id, true, true);
return array($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm, $forumcompleteautocm);
}
diff --git a/course/upgrade.txt b/course/upgrade.txt
index d7b19cb0929..ddde77d300f 100644
--- a/course/upgrade.txt
+++ b/course/upgrade.txt
@@ -92,6 +92,11 @@ course formats don't have their own renderer.
* The method navigation() in the core_course_bulk_activity_completion_renderer class has been deprecated as the tabs navigation
structure in the course competency pages has been replaced with tertiary navigation. To render the navigation, please
render_course_completion_action_bar() instead.
+* The following functions have been deprecated in favour partial course cache rebuild:
+ - course_purge_section_cache (replaced by course_modinfo::purge_course_section_cache_by_id() and course_modinfo::purge_course_section_cache_by_number())
+ - course_purge_module_cache (replaced by course_modinfo::purge_course_module_cache())
+ - get_array_of_activities (replaced by course_modinfo::get_array_of_activities() for better performance)
+* New course_modinfo::purge_course_cache function was created to purge the cache of a given course.
=== 3.11 ===
* A new callback xxx_coursemodule_definition_after_data that allows plugins to extend activity forms after the data is set.
diff --git a/lib/db/caches.php b/lib/db/caches.php
index 55cb49ef222..8e65c01e76b 100644
--- a/lib/db/caches.php
+++ b/lib/db/caches.php
@@ -227,8 +227,16 @@ $definitions = array(
'simplekeys' => true,
'ttl' => 3600,
),
- // Accumulated information about course modules and sections used to print course view page (user-independed).
- // Used in function get_fast_modinfo(), reset in function rebuild_course_cache().
+ // Accumulated information about course modules and sections used to print course view page (user-independent).
+ // Used in functions:
+ // - course_modinfo::build_course_section_cache()
+ // - course_modinfo::inner_build_course_cache()
+ // - get_array_of_activities()
+ // Reset/update in functions:
+ // - rebuild_course_cache()
+ // - course_modinfo::purge_module_cache()
+ // - course_modinfo::purge_section_cache()
+ // - remove_course_contents().
'coursemodinfo' => array(
'mode' => cache_store::MODE_APPLICATION,
'simplekeys' => true,
diff --git a/lib/deprecatedlib.php b/lib/deprecatedlib.php
index 305ae3db94e..9d9208c882c 100644
--- a/lib/deprecatedlib.php
+++ b/lib/deprecatedlib.php
@@ -3687,4 +3687,60 @@ function print_grade_plugin_selector($plugin_info, $active_type, $active_plugin,
// only one option - no plugin selector needed
return '';
}
+
+ /**
+ * Purge the cache of a course section.
+ *
+ * $sectioninfo must have following attributes:
+ * - course: course id
+ * - section: section number
+ *
+ * @param object $sectioninfo section info
+ * @return void
+ * @deprecated since Moodle 4.0. Please use {@link course_modinfo::purge_course_section_cache_by_id()}
+ * or {@link course_modinfo::purge_course_section_cache_by_number()} instead.
+ */
+ function course_purge_section_cache(object $sectioninfo): void {
+ debugging(__FUNCTION__ . '() is deprecated. ' .
+ 'Please use course_modinfo::purge_course_section_cache_by_id() ' .
+ 'or course_modinfo::purge_course_section_cache_by_number() instead.',
+ DEBUG_DEVELOPER);
+ $sectionid = $sectioninfo->section;
+ $courseid = $sectioninfo->course;
+ course_modinfo::purge_course_section_cache_by_id($courseid, $sectionid);
+ }
+
+ /**
+ * Purge the cache of a course module.
+ *
+ * $cm must have following attributes:
+ * - id: cmid
+ * - course: course id
+ *
+ * @param cm_info|stdClass $cm course module
+ * @return void
+ * @deprecated since Moodle 4.0. Please use {@link course_modinfo::purge_course_module_cache()} instead.
+ */
+ function course_purge_module_cache($cm): void {
+ debugging(__FUNCTION__ . '() is deprecated. ' . 'Please use course_modinfo::purge_course_module_cache() instead.',
+ DEBUG_DEVELOPER);
+ $cmid = $cm->id;
+ $courseid = $cm->course;
+ course_modinfo::purge_course_module_cache($courseid, $cmid);
+ }
+}
+
+/**
+ * For a given course, returns an array of course activity objects
+ * Each item in the array contains he following properties:
+ *
+ * @param int $courseid course id
+ * @param bool $usecache get activities from cache if modinfo exists when $usecache is true
+ * @return array list of activities
+ * @deprecated since Moodle 4.0. Please use {@link course_modinfo::get_array_of_activities()} instead.
+ */
+function get_array_of_activities(int $courseid, bool $usecache = false): array {
+ debugging(__FUNCTION__ . '() is deprecated. ' . 'Please use course_modinfo::get_array_of_activities() instead.',
+ DEBUG_DEVELOPER);
+ return course_modinfo::get_array_of_activities(get_course($courseid), $usecache);
}
diff --git a/lib/modinfolib.php b/lib/modinfolib.php
index 9e148133d5c..bf20367db5d 100644
--- a/lib/modinfolib.php
+++ b/lib/modinfolib.php
@@ -476,13 +476,13 @@ class course_modinfo {
// Retrieve modinfo from cache. If not present or cacherev mismatches, call rebuild and retrieve again.
$coursemodinfo = $cachecoursemodinfo->get_versioned($course->id, $course->cacherev);
- if (!$coursemodinfo) {
+ if ($coursemodinfo === false || ($course->cacherev != $coursemodinfo->cacherev)) {
$lock = self::get_course_cache_lock($course->id);
try {
// Only actually do the build if it's still needed after getting the lock (not if
// somebody else, who might have been holding the lock, built it already).
$coursemodinfo = $cachecoursemodinfo->get_versioned($course->id, $course->cacherev);
- if (!$coursemodinfo) {
+ if ($coursemodinfo === false || ($course->cacherev != $coursemodinfo->cacherev)) {
$coursemodinfo = self::inner_build_course_cache($course, $lock);
}
} finally {
@@ -514,7 +514,7 @@ class course_modinfo {
' does not have context. Rebuilding cache for course '. $course->id);
// Re-request the course record from DB as well, don't use get_course() here.
$course = $DB->get_record('course', array('id' => $course->id), '*', MUST_EXIST);
- $coursemodinfo = self::build_course_cache($course);
+ $coursemodinfo = self::build_course_cache($course, true);
break;
}
}
@@ -588,23 +588,37 @@ class course_modinfo {
* in some other way.)
*
* @param stdClass $course Course object (must contain fields
+ * @param boolean $usecache use cached section info if exists, use true for partial course rebuild
* @return array Information about sections, indexed by section number (not id)
*/
- protected static function build_course_section_cache($course) {
+ protected static function build_course_section_cache(\stdClass $course, bool $usecache = false): array {
global $DB;
// Get section data
$sections = $DB->get_records('course_sections', array('course' => $course->id), 'section',
'section, id, course, name, summary, summaryformat, sequence, visible, availability');
- $compressedsections = array();
+ $compressedsections = [];
+ $courseformat = course_get_format($course);
+
+ if ($usecache) {
+ $cachecoursemodinfo = \cache::make('core', 'coursemodinfo');
+ $coursemodinfo = $cachecoursemodinfo->get_versioned($course->id, $course->cacherev);
+ if ($coursemodinfo !== false) {
+ $compressedsections = $coursemodinfo->sectioncache;
+ }
+ }
$formatoptionsdef = course_get_format($course)->section_format_options();
// Remove unnecessary data and add availability
foreach ($sections as $number => $section) {
+ $sectioninfocached = isset($compressedsections[$number]);
+ if ($sectioninfocached) {
+ continue;
+ }
// Add cached options from course format to $section object
foreach ($formatoptionsdef as $key => $option) {
if (!empty($option['cache'])) {
- $formatoptions = course_get_format($course)->get_format_options($section);
+ $formatoptions = $courseformat->get_format_options($section);
if (!array_key_exists('cachedefault', $option) || $option['cachedefault'] !== $formatoptions[$key]) {
$section->$key = $formatoptions[$key];
}
@@ -615,6 +629,7 @@ class course_modinfo {
section_info::convert_for_section_cache($compressedsections[$number]);
}
+ ksort($compressedsections);
return $compressedsections;
}
@@ -652,19 +667,20 @@ class course_modinfo {
*
* @param stdClass $course object from DB table course. Must have property 'id'
* but preferably should have all cached fields.
+ * @param boolean $partialrebuild Indicate if it's partial course cache rebuild or not
* @return stdClass object with all cached keys of the course plus fields modinfo and sectioncache.
* The same object is stored in MUC
* @throws moodle_exception if course is not found (if $course object misses some of the
* necessary fields it is re-requested from database)
*/
- public static function build_course_cache($course) {
+ public static function build_course_cache(\stdClass $course, bool $partialrebuild = false): \stdClass {
if (empty($course->id)) {
throw new coding_exception('Object $course is missing required property \id\'');
}
$lock = self::get_course_cache_lock($course->id);
try {
- return self::inner_build_course_cache($course, $lock);
+ return self::inner_build_course_cache($course, $lock, $partialrebuild);
} finally {
$lock->release();
}
@@ -675,9 +691,11 @@ class course_modinfo {
*
* @param stdClass $course object from DB table course
* @param \core\lock\lock $lock Lock object - not actually used, just there to indicate you have a lock
+ * @param bool $partialrebuild Indicate if it's partial course cache rebuild or not
* @return stdClass Course object that has been stored in MUC
*/
- protected static function inner_build_course_cache($course, \core\lock\lock $lock) {
+ protected static function inner_build_course_cache(\stdClass $course, \core\lock\lock $lock,
+ bool $partialrebuild = false): \stdClass {
global $DB, $CFG;
require_once("{$CFG->dirroot}/course/lib.php");
@@ -688,8 +706,8 @@ class course_modinfo {
// Retrieve all information about activities and sections.
$coursemodinfo = new stdClass();
- $coursemodinfo->modinfo = get_array_of_activities($course->id);
- $coursemodinfo->sectioncache = self::build_course_section_cache($course);
+ $coursemodinfo->modinfo = self::get_array_of_activities($course, $partialrebuild);
+ $coursemodinfo->sectioncache = self::build_course_section_cache($course, $partialrebuild);
foreach (self::$cachedfields as $key) {
$coursemodinfo->$key = $course->$key;
}
@@ -698,6 +716,264 @@ class course_modinfo {
$cachecoursemodinfo->set_versioned($course->id, $course->cacherev, $coursemodinfo);
return $coursemodinfo;
}
+
+ /**
+ * Purge the cache of a course section by its id.
+ *
+ * @param int $courseid The course to purge cache in
+ * @param int $sectionid The section _id_ to purge
+ */
+ public static function purge_course_section_cache_by_id(int $courseid, int $sectionid): void {
+ $course = get_course($courseid);
+ $cache = cache::make('core', 'coursemodinfo');
+ $cache->acquire_lock($course->id);
+ $coursemodinfo = $cache->get_versioned($course->id, $course->cacherev);
+ if ($coursemodinfo !== false) {
+ foreach ($coursemodinfo->sectioncache as $sectionno => $sectioncache) {
+ if ($sectioncache->id == $sectionid) {
+ $coursemodinfo->cacherev = -1;
+ unset($coursemodinfo->sectioncache[$sectionno]);
+ $cache->set_versioned($course->id, $course->cacherev, $coursemodinfo);
+ break;
+ }
+ }
+ }
+ $cache->release_lock($course->id);
+ }
+
+ /**
+ * Purge the cache of a course section by its number.
+ *
+ * @param int $courseid The course to purge cache in
+ * @param int $sectionno The section number to purge
+ */
+ public static function purge_course_section_cache_by_number(int $courseid, int $sectionno): void {
+ $course = get_course($courseid);
+ $cache = cache::make('core', 'coursemodinfo');
+ $cache->acquire_lock($course->id);
+ $coursemodinfo = $cache->get_versioned($course->id, $course->cacherev);
+ if ($coursemodinfo !== false && array_key_exists($sectionno, $coursemodinfo->sectioncache)) {
+ $coursemodinfo->cacherev = -1;
+ unset($coursemodinfo->sectioncache[$sectionno]);
+ $cache->set_versioned($course->id, $course->cacherev, $coursemodinfo);
+ }
+ $cache->release_lock($course->id);
+ }
+
+ /**
+ * Purge the cache of a course module.
+ *
+ * @param int $courseid Course id
+ * @param int $cmid Course module id
+ */
+ public static function purge_course_module_cache(int $courseid, int $cmid): void {
+ $course = get_course($courseid);
+ $cache = cache::make('core', 'coursemodinfo');
+ $cache->acquire_lock($course->id);
+ $coursemodinfo = $cache->get_versioned($course->id, $course->cacherev);
+ $hascache = ($coursemodinfo !== false) && array_key_exists($cmid, $coursemodinfo->modinfo);
+ if ($hascache) {
+ $coursemodinfo->cacherev = -1;
+ unset($coursemodinfo->modinfo[$cmid]);
+ $cache->set_versioned($course->id, $course->cacherev, $coursemodinfo);
+ $coursemodinfo = $cache->get_versioned($course->id, $course->cacherev);
+ }
+ $cache->release_lock($course->id);
+ }
+
+ /**
+ * For a given course, returns an array of course activity objects
+ *
+ * @param stdClass $course Course object
+ * @param bool $usecache get activities from cache if modinfo exists when $usecache is true
+ * @return array list of activities
+ */
+ public static function get_array_of_activities(stdClass $course, bool $usecache = false): array {
+ global $CFG, $DB;
+
+ if (empty($course)) {
+ throw new moodle_exception('courseidnotfound');
+ }
+
+ $rawmods = get_course_mods($course->id);
+ if (empty($rawmods)) {
+ return [];
+ }
+
+ $mods = [];
+ if ($usecache) {
+ // Get existing cache.
+ $cachecoursemodinfo = cache::make('core', 'coursemodinfo');
+ $coursemodinfo = $cachecoursemodinfo->get_versioned($course->id, $course->cacherev);
+ if ($coursemodinfo !== false) {
+ $mods = $coursemodinfo->modinfo;
+ }
+ }
+
+ $courseformat = course_get_format($course);
+
+ if ($sections = $DB->get_records('course_sections', ['course' => $course->id],
+ 'section ASC', 'id,section,sequence,visible')) {
+ // First check and correct obvious mismatches between course_sections.sequence and course_modules.section.
+ if ($errormessages = course_integrity_check($course->id, $rawmods, $sections)) {
+ debugging(join('
', $errormessages));
+ $rawmods = get_course_mods($course->id);
+ $sections = $DB->get_records('course_sections', ['course' => $course->id],
+ 'section ASC', 'id,section,sequence,visible');
+ }
+ // Build array of activities.
+ foreach ($sections as $section) {
+ if (!empty($section->sequence)) {
+ $cmids = explode(",", $section->sequence);
+ $numberofmods = count($cmids);
+ foreach ($cmids as $cmid) {
+ // Activity does not exist in the database.
+ $notexistindb = empty($rawmods[$cmid]);
+ $activitycached = isset($mods[$cmid]);
+ if ($activitycached || $notexistindb) {
+ continue;
+ }
+
+ // Adjust visibleoncoursepage, value in DB may not respect format availability.
+ $rawmods[$cmid]->visibleoncoursepage = (!$rawmods[$cmid]->visible
+ || $rawmods[$cmid]->visibleoncoursepage
+ || empty($CFG->allowstealth)
+ || !$courseformat->allow_stealth_module_visibility($rawmods[$cmid], $section)) ? 1 : 0;
+
+ $mods[$cmid] = new stdClass();
+ $mods[$cmid]->id = $rawmods[$cmid]->instance;
+ $mods[$cmid]->cm = $rawmods[$cmid]->id;
+ $mods[$cmid]->mod = $rawmods[$cmid]->modname;
+
+ // Oh dear. Inconsistent names left here for backward compatibility.
+ $mods[$cmid]->section = $section->section;
+ $mods[$cmid]->sectionid = $rawmods[$cmid]->section;
+
+ $mods[$cmid]->module = $rawmods[$cmid]->module;
+ $mods[$cmid]->added = $rawmods[$cmid]->added;
+ $mods[$cmid]->score = $rawmods[$cmid]->score;
+ $mods[$cmid]->idnumber = $rawmods[$cmid]->idnumber;
+ $mods[$cmid]->visible = $rawmods[$cmid]->visible;
+ $mods[$cmid]->visibleoncoursepage = $rawmods[$cmid]->visibleoncoursepage;
+ $mods[$cmid]->visibleold = $rawmods[$cmid]->visibleold;
+ $mods[$cmid]->groupmode = $rawmods[$cmid]->groupmode;
+ $mods[$cmid]->groupingid = $rawmods[$cmid]->groupingid;
+ $mods[$cmid]->indent = $rawmods[$cmid]->indent;
+ $mods[$cmid]->completion = $rawmods[$cmid]->completion;
+ $mods[$cmid]->extra = "";
+ $mods[$cmid]->completiongradeitemnumber =
+ $rawmods[$cmid]->completiongradeitemnumber;
+ $mods[$cmid]->completionpassgrade = $rawmods[$cmid]->completionpassgrade;
+ $mods[$cmid]->completionview = $rawmods[$cmid]->completionview;
+ $mods[$cmid]->completionexpected = $rawmods[$cmid]->completionexpected;
+ $mods[$cmid]->showdescription = $rawmods[$cmid]->showdescription;
+ $mods[$cmid]->availability = $rawmods[$cmid]->availability;
+ $mods[$cmid]->deletioninprogress = $rawmods[$cmid]->deletioninprogress;
+ $mods[$cmid]->downloadcontent = $rawmods[$cmid]->downloadcontent;
+
+ $modname = $mods[$cmid]->mod;
+ $functionname = $modname . "_get_coursemodule_info";
+
+ if (!file_exists("$CFG->dirroot/mod/$modname/lib.php")) {
+ continue;
+ }
+
+ include_once("$CFG->dirroot/mod/$modname/lib.php");
+
+ if ($hasfunction = function_exists($functionname)) {
+ if ($info = $functionname($rawmods[$cmid])) {
+ if (!empty($info->icon)) {
+ $mods[$cmid]->icon = $info->icon;
+ }
+ if (!empty($info->iconcomponent)) {
+ $mods[$cmid]->iconcomponent = $info->iconcomponent;
+ }
+ if (!empty($info->name)) {
+ $mods[$cmid]->name = $info->name;
+ }
+ if ($info instanceof cached_cm_info) {
+ // When using cached_cm_info you can include three new fields.
+ // That aren't available for legacy code.
+ if (!empty($info->content)) {
+ $mods[$cmid]->content = $info->content;
+ }
+ if (!empty($info->extraclasses)) {
+ $mods[$cmid]->extraclasses = $info->extraclasses;
+ }
+ if (!empty($info->iconurl)) {
+ // Convert URL to string as it's easier to store.
+ // Also serialized object contains \0 byte,
+ // ... and can not be written to Postgres DB.
+ $url = new moodle_url($info->iconurl);
+ $mods[$cmid]->iconurl = $url->out(false);
+ }
+ if (!empty($info->onclick)) {
+ $mods[$cmid]->onclick = $info->onclick;
+ }
+ if (!empty($info->customdata)) {
+ $mods[$cmid]->customdata = $info->customdata;
+ }
+ } else {
+ // When using a stdclass, the (horrible) deprecated ->extra field,
+ // ... that is available for BC.
+ if (!empty($info->extra)) {
+ $mods[$cmid]->extra = $info->extra;
+ }
+ }
+ }
+ }
+ // When there is no modname_get_coursemodule_info function,
+ // ... but showdescriptions is enabled, then we use the 'intro',
+ // ... and 'introformat' fields in the module table.
+ if (!$hasfunction && $rawmods[$cmid]->showdescription) {
+ if ($modvalues = $DB->get_record($rawmods[$cmid]->modname,
+ ['id' => $rawmods[$cmid]->instance], 'name, intro, introformat')) {
+ // Set content from intro and introformat. Filters are disabled.
+ // Because we filter it with format_text at display time.
+ $mods[$cmid]->content = format_module_intro($rawmods[$cmid]->modname,
+ $modvalues, $rawmods[$cmid]->id, false);
+
+ // To save making another query just below, put name in here.
+ $mods[$cmid]->name = $modvalues->name;
+ }
+ }
+ if (!isset($mods[$cmid]->name)) {
+ $mods[$cmid]->name = $DB->get_field($rawmods[$cmid]->modname, "name",
+ ["id" => $rawmods[$cmid]->instance]);
+ }
+
+ // Minimise the database size by unsetting default options when they are 'empty'.
+ // This list corresponds to code in the cm_info constructor.
+ foreach (['idnumber', 'groupmode', 'groupingid',
+ 'indent', 'completion', 'extra', 'extraclasses', 'iconurl', 'onclick', 'content',
+ 'icon', 'iconcomponent', 'customdata', 'availability', 'completionview',
+ 'completionexpected', 'score', 'showdescription', 'deletioninprogress'] as $property) {
+ if (property_exists($mods[$cmid], $property) &&
+ empty($mods[$cmid]->{$property})) {
+ unset($mods[$cmid]->{$property});
+ }
+ }
+ // Special case: this value is usually set to null, but may be 0.
+ if (property_exists($mods[$cmid], 'completiongradeitemnumber') &&
+ is_null($mods[$cmid]->completiongradeitemnumber)) {
+ unset($mods[$cmid]->completiongradeitemnumber);
+ }
+ }
+ }
+ }
+ }
+ return $mods;
+ }
+
+ /**
+ * Purge the cache of a given course
+ *
+ * @param int $courseid Course id
+ */
+ public static function purge_course_cache(int $courseid): void {
+ $cachemodinfo = cache::make('core', 'coursemodinfo');
+ $cachemodinfo->delete($courseid);
+ }
}
@@ -2450,9 +2726,21 @@ function get_course_and_cm_from_instance($instanceorid, $modulename, $courseorid
* @param int $courseid id of course to rebuild, empty means all
* @param boolean $clearonly only clear the cache, gets rebuild automatically on the fly.
* Recommended to set to true to avoid unnecessary multiple rebuilding.
+ * @param boolean $partialrebuild will not delete the whole cache when it's true.
+ * use purge_module_cache() or purge_section_cache() must be
+ * called before when partialrebuild is true.
+ * use purge_module_cache() to invalidate mod cache.
+ * use purge_section_cache() to invalidate section cache.
+ *
+ * @return void
+ * @throws coding_exception
*/
-function rebuild_course_cache($courseid=0, $clearonly=false) {
- global $COURSE, $SITE, $DB, $CFG;
+function rebuild_course_cache(int $courseid = 0, bool $clearonly = false, bool $partialrebuild = false): void {
+ global $COURSE, $SITE, $DB;
+
+ if ($courseid == 0 and $partialrebuild) {
+ throw new coding_exception('partialrebuild only works when a valid course id is provided.');
+ }
// Function rebuild_course_cache() can not be called during upgrade unless it's clear only.
if (!$clearonly && !upgrade_ensure_not_running(true)) {
@@ -2468,7 +2756,10 @@ function rebuild_course_cache($courseid=0, $clearonly=false) {
if (empty($courseid)) {
// Clearing caches for all courses.
increment_revision_number('course', 'cacherev', '');
- $cachecoursemodinfo->purge();
+ if (!$partialrebuild) {
+ $cachecoursemodinfo->purge();
+ }
+ // Clear memory static cache.
course_modinfo::clear_instance_cache();
// Update global values too.
$sitecacherev = $DB->get_field('course', 'cacherev', array('id' => SITEID));
@@ -2481,7 +2772,11 @@ function rebuild_course_cache($courseid=0, $clearonly=false) {
} else {
// Clearing cache for one course, make sure it is deleted from user request cache as well.
increment_revision_number('course', 'cacherev', 'id = :id', array('id' => $courseid));
- $cachecoursemodinfo->delete($courseid);
+ if (!$partialrebuild) {
+ // Purge all course modinfo.
+ $cachecoursemodinfo->delete($courseid);
+ }
+ // Clear memory static cache.
course_modinfo::clear_instance_cache($courseid);
// Update global values too.
if ($courseid == $COURSE->id || $courseid == $SITE->id) {
@@ -2506,10 +2801,13 @@ function rebuild_course_cache($courseid=0, $clearonly=false) {
core_php_time_limit::raise(); // this could take a while! MDL-10954
}
- $rs = $DB->get_recordset("course", $select,'','id,'.join(',', course_modinfo::$cachedfields));
+ $fields = 'id,' . join(',', course_modinfo::$cachedfields);
+ $sort = '';
+ $rs = $DB->get_recordset("course", $select, $sort, $fields);
+
// Rebuild cache for each course.
foreach ($rs as $course) {
- course_modinfo::build_course_cache($course);
+ course_modinfo::build_course_cache($course, $partialrebuild);
}
$rs->close();
}
diff --git a/lib/moodlelib.php b/lib/moodlelib.php
index ee60a46c6e3..eb4c126f75a 100644
--- a/lib/moodlelib.php
+++ b/lib/moodlelib.php
@@ -5347,8 +5347,7 @@ function remove_course_contents($courseid, $showfeedback = true, array $options
fulldelete($CFG->dataroot.'/'.$course->id);
// Delete from cache to reduce the cache size especially makes sense in case of bulk course deletion.
- $cachemodinfo = cache::make('core', 'coursemodinfo');
- $cachemodinfo->delete($courseid);
+ course_modinfo::purge_course_cache($courseid);
// Trigger a course content deleted event.
$event = \core\event\course_content_deleted::create(array(
diff --git a/lib/tests/modinfolib_test.php b/lib/tests/modinfolib_test.php
index 50d7431dbf9..da19910af82 100644
--- a/lib/tests/modinfolib_test.php
+++ b/lib/tests/modinfolib_test.php
@@ -1004,4 +1004,96 @@ class modinfolib_test extends advanced_testcase {
],
];
}
+
+ /**
+ * Test purge_section_cache_by_id method
+ *
+ * @covers \course_modinfo::purge_course_section_cache_by_id
+ * @return void
+ */
+ public function test_purge_section_cache_by_id(): void {
+ $this->resetAfterTest();
+ $this->setAdminUser();
+ $cache = cache::make('core', 'coursemodinfo');
+
+ // Generate the course and pre-requisite section.
+ $course = $this->getDataGenerator()->create_course(['format' => 'topics', 'numsections' => 3], ['createsections' => true]);
+ // Reset course cache.
+ rebuild_course_cache($course->id, true);
+ // Build course cache.
+ get_fast_modinfo($course->id);
+ // Get the course modinfo cache.
+ $coursemodinfo = $cache->get_versioned($course->id, $course->cacherev);
+ // Get the section cache.
+ $sectioncaches = $coursemodinfo->sectioncache;
+
+ // Make sure that we will have 4 section caches here.
+ $this->assertCount(4, $sectioncaches);
+ $this->assertArrayHasKey(0, $sectioncaches);
+ $this->assertArrayHasKey(1, $sectioncaches);
+ $this->assertArrayHasKey(2, $sectioncaches);
+ $this->assertArrayHasKey(3, $sectioncaches);
+
+ // Purge cache for the section by id.
+ course_modinfo::purge_course_section_cache_by_id($course->id, $sectioncaches[1]->id);
+ // Get the course modinfo cache.
+ $coursemodinfo = $cache->get_versioned($course->id, $course->cacherev);
+ // Get the section cache.
+ $sectioncaches = $coursemodinfo->sectioncache;
+
+ // Make sure that we will have 3 section caches left.
+ $this->assertCount(3, $sectioncaches);
+ $this->assertArrayNotHasKey(1, $sectioncaches);
+ $this->assertArrayHasKey(0, $sectioncaches);
+ $this->assertArrayHasKey(2, $sectioncaches);
+ $this->assertArrayHasKey(3, $sectioncaches);
+ // Make sure that the cacherev will be reset.
+ $this->assertEquals(-1, $coursemodinfo->cacherev);
+ }
+
+ /**
+ * Test purge_section_cache_by_number method
+ *
+ * @covers \course_modinfo::purge_course_section_cache_by_number
+ * @return void
+ */
+ public function test_section_cache_by_number(): void {
+ $this->resetAfterTest();
+ $this->setAdminUser();
+ $cache = cache::make('core', 'coursemodinfo');
+
+ // Generate the course and pre-requisite section.
+ $course = $this->getDataGenerator()->create_course(['format' => 'topics', 'numsections' => 3], ['createsections' => true]);
+ // Reset course cache.
+ rebuild_course_cache($course->id, true);
+ // Build course cache.
+ get_fast_modinfo($course->id);
+ // Get the course modinfo cache.
+ $coursemodinfo = $cache->get_versioned($course->id, $course->cacherev);
+ // Get the section cache.
+ $sectioncaches = $coursemodinfo->sectioncache;
+
+ // Make sure that we will have 4 section caches here.
+ $this->assertCount(4, $sectioncaches);
+ $this->assertArrayHasKey(0, $sectioncaches);
+ $this->assertArrayHasKey(1, $sectioncaches);
+ $this->assertArrayHasKey(2, $sectioncaches);
+ $this->assertArrayHasKey(3, $sectioncaches);
+
+ // Purge cache for the section with section number is 1.
+ course_modinfo::purge_course_section_cache_by_number($course->id, 1);
+ // Get the course modinfo cache.
+ $coursemodinfo = $cache->get_versioned($course->id, $course->cacherev);
+ // Get the section cache.
+ $sectioncaches = $coursemodinfo->sectioncache;
+
+ // Make sure that we will have 3 section caches left.
+ $this->assertCount(3, $sectioncaches);
+ $this->assertArrayNotHasKey(1, $sectioncaches);
+ $this->assertArrayHasKey(0, $sectioncaches);
+ $this->assertArrayHasKey(2, $sectioncaches);
+ $this->assertArrayHasKey(3, $sectioncaches);
+ // Make sure that the cacherev will be reset.
+ $this->assertEquals(-1, $coursemodinfo->cacherev);
+ }
}
diff --git a/lib/upgrade.txt b/lib/upgrade.txt
index 70050362a18..721b84b4422 100644
--- a/lib/upgrade.txt
+++ b/lib/upgrade.txt
@@ -42,7 +42,9 @@ information provided here is intended especially for developers.
* The completion_info function display_help_icon() which returned the 'Your progress' help icon has been deprecated and
should no longer be used.
* The completion_info function print_help_icon() which has been deprecated since Moodle 2.0 should no longer be used.
-* @babel/polyfill has been removed in favour of corejs@3
+* @babel/polyfill has been removed in favour of corejs@3.
+* A new parameter $partialrebuild has been added to the rebuild_course_cache to invalidate the cache
+ of the section or module only, not the whole course cache
* A new parameter $isbulkupdate has been added to the following functions:
- grade_category::update()
- grade_category::insert()
diff --git a/mod/chat/lib.php b/mod/chat/lib.php
index b7cd3968c5f..0cce046391d 100644
--- a/mod/chat/lib.php
+++ b/mod/chat/lib.php
@@ -121,7 +121,8 @@ function padding($n) {
* @return int
*/
function chat_add_instance($chat) {
- global $DB;
+ global $DB, $CFG;
+ require_once($CFG->dirroot . '/course/lib.php');
$chat->timemodified = time();
$chat->chattime = chat_calculate_next_chat_time($chat->schedule, $chat->chattime);
@@ -706,10 +707,13 @@ function chat_update_chat_times($chatid=0) {
$chat->chattime = chat_calculate_next_chat_time($chat->schedule, $chat->chattime);
if ($originalchattime != $chat->chattime) {
$courseids[] = $chat->course;
- }
- $DB->update_record("chat", $chat);
- $event = new stdClass(); // Update calendar too.
+ $DB->update_record("chat", $chat);
+ $cm = get_coursemodule_from_instance('chat', $chat->id, $chat->course);
+ \course_modinfo::purge_course_module_cache($cm->course, $cm->id);
+ }
+
+ $event = new stdClass(); // Update calendar too.
$cond = "modulename='chat' AND eventtype = :eventtype AND instance = :chatid AND timestart <> :chattime";
$params = ['chattime' => $chat->chattime, 'eventtype' => CHAT_EVENT_TYPE_CHATTIME, 'chatid' => $chat->id];
@@ -723,7 +727,7 @@ function chat_update_chat_times($chatid=0) {
$courseids = array_unique($courseids);
foreach ($courseids as $courseid) {
- rebuild_course_cache($courseid, true);
+ rebuild_course_cache($courseid, true, true);
}
}
diff --git a/mod/lti/locallib.php b/mod/lti/locallib.php
index 585f6da101a..d69aacf0c84 100644
--- a/mod/lti/locallib.php
+++ b/mod/lti/locallib.php
@@ -2837,14 +2837,23 @@ function lti_update_type($type, $config) {
}
require_once($CFG->libdir.'/modinfolib.php');
if ($clearcache) {
- $sql = "SELECT DISTINCT course
- FROM {lti}
- WHERE typeid = ?";
+ $sql = "SELECT cm.id, cm.course
+ FROM {course_modules} cm
+ JOIN {modules} m ON cm.module = m.id
+ JOIN {lti} l ON l.course = cm.course
+ WHERE m.name = :name AND l.typeid = :typeid";
- $courses = $DB->get_fieldset_sql($sql, array($type->id));
+ $rs = $DB->get_recordset_sql($sql, ['name' => 'lti', 'typeid' => $type->id]);
- foreach ($courses as $courseid) {
- rebuild_course_cache($courseid, true);
+ $courseids = [];
+ foreach ($rs as $record) {
+ $courseids[] = $record->course;
+ \course_modinfo::purge_course_module_cache($record->course, $record->id);
+ }
+ $rs->close();
+ $courseids = array_unique($courseids);
+ foreach ($courseids as $courseid) {
+ rebuild_course_cache($courseid, false, true);
}
}
}
diff --git a/mod/page/lib.php b/mod/page/lib.php
index dae27693bb7..bda4a4fd616 100644
--- a/mod/page/lib.php
+++ b/mod/page/lib.php
@@ -201,7 +201,7 @@ function page_delete_instance($id) {
* "extra" information that may be needed when printing
* this activity in a course listing.
*
- * See {@link get_array_of_activities()} in course/lib.php
+ * See {@link course_modinfo::get_array_of_activities()}
*
* @param stdClass $coursemodule
* @return cached_cm_info Info to customise main page display
diff --git a/mod/resource/lib.php b/mod/resource/lib.php
index 726fd47ca4d..563d2422bc5 100644
--- a/mod/resource/lib.php
+++ b/mod/resource/lib.php
@@ -192,7 +192,7 @@ function resource_delete_instance($id) {
* "extra" information that may be needed when printing
* this activity in a course listing.
*
- * See {@link get_array_of_activities()} in course/lib.php
+ * See {@link course_modinfo::get_array_of_activities()}
*
* @param stdClass $coursemodule
* @return cached_cm_info info
diff --git a/mod/url/lib.php b/mod/url/lib.php
index 169c8a6784d..dae393ac800 100644
--- a/mod/url/lib.php
+++ b/mod/url/lib.php
@@ -203,7 +203,7 @@ function url_delete_instance($id) {
* "extra" information that may be needed when printing
* this activity in a course listing.
*
- * See {@link get_array_of_activities()} in course/lib.php
+ * See {@link course_modinfo::get_array_of_activities()}
*
* @param object $coursemodule
* @return cached_cm_info info