diff --git a/availability/condition/completion/tests/behat/availability_completion.feature b/availability/condition/completion/tests/behat/availability_completion.feature index 0f4c121d968..67743d3f1c0 100644 --- a/availability/condition/completion/tests/behat/availability_completion.feature +++ b/availability/condition/completion/tests/behat/availability_completion.feature @@ -54,3 +54,64 @@ 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" + + When I log out + And I log in as "student1" + And 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. + Then I should not see "Page 2" in the "region-main" "region" + + When 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" + + When 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 + Then 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..fdf2f705006 100644 --- a/availability/condition/date/classes/condition.php +++ b/availability/condition/date/classes/condition.php @@ -292,14 +292,15 @@ class condition extends \core_availability\condition { $updatesection->availability = json_encode($tree->save()); $updatesection->timemodified = time(); $DB->update_record('course_sections', $updatesection); + course_purge_section_cache($section); $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..00216dc4441 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_purge_module_cache($this->cm); // 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 23bb9b2d11e..1cfc6279eeb 100644 --- a/course/format/classes/base.php +++ b/course/format/classes/base.php @@ -1009,7 +1009,15 @@ abstract class base { } } if ($needrebuild) { - rebuild_course_cache($this->courseid, true); + if ($sectionid) { + $sectionrecord = $DB->get_record('course_sections', ['id' => $sectionid], 'course, section', MUST_EXIST); + course_purge_section_cache($sectionrecord); + // 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. @@ -1277,7 +1285,7 @@ abstract class base { } if (!is_object($section)) { $section = $DB->get_record('course_sections', array('course' => $this->get_courseid(), 'section' => $section), - 'id,section,sequence,summary'); + 'id,course,section,sequence,summary'); } if (!$section || !$section->section) { // Not possible to delete 0-section. @@ -1314,7 +1322,9 @@ 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); + course_purge_section_cache($section); + // 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 43733242a18..03d454fe95d 100644 --- a/course/lib.php +++ b/course/lib.php @@ -389,8 +389,12 @@ function course_integrity_check($courseid, $rawmods = null, $sections = null, $f /** * 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 */ -function get_array_of_activities($courseid) { +function get_array_of_activities(int $courseid, bool $usecache = false): array { // cm - course module id // mod - name of the module (eg forum) // section - the number of the section (eg week or topic) @@ -406,12 +410,21 @@ function get_array_of_activities($courseid) { throw new moodle_exception('courseidnotfound'); } - $mod = array(); - $rawmods = get_course_mods($courseid); if (empty($rawmods)) { - return $mod; // always return array + return []; } + + $mods = []; + if ($usecache) { + // Get existing cache. + $cachecoursemodinfo = cache::make('core', 'coursemodinfo'); + $coursemodinfo = $cachecoursemodinfo->get($courseid); + if ($coursemodinfo !== false) { + $mods = $coursemodinfo->modinfo; + } + } + $courseformat = course_get_format($course); if ($sections = $DB->get_records('course_sections', array('course' => $courseid), @@ -423,140 +436,152 @@ function get_array_of_activities($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; + // Build array of activities. + foreach ($sections as $section) { + if (!empty($section->sequence)) { + $cmids = explode(",", $section->sequence); + $numberofmods = count($cmids); + for ($order = 0; $order < $numberofmods; $order++) { + $cmid = $cmids[$order]; + // Activity not exist in database. + $notexistindb = empty($rawmods[$cmid]); + $activitycached = isset($mods[$cmid]); + if ($activitycached || $notexistindb) { + continue; + } + $modposition = ($order === 0) ? 0 : array_search($cmids[$order - 1], array_keys($mods)) + 1; + $mods = array_slice($mods, 0, $modposition, true) + + [$cmid => new stdClass()] + + array_slice($mods, $modposition, null, true); + + // 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]->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. - $mod[$seq]->section = $section->section; - $mod[$seq]->sectionid = $rawmods[$seq]->section; + $mods[$cmid]->section = $section->section; + $mods[$cmid]->sectionid = $rawmods[$cmid]->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; + $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; - $modname = $mod[$seq]->mod; - $functionname = $modname."_get_coursemodule_info"; + $modname = $mods[$cmid]->mod; + $functionname = $modname . "_get_coursemodule_info"; - if (!file_exists("$CFG->dirroot/mod/$modname/lib.php")) { - continue; - } + if (!file_exists("$CFG->dirroot/mod/$modname/lib.php")) { + continue; + } - include_once("$CFG->dirroot/mod/$modname/lib.php"); + 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); + 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 - $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)); - } + // 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 (array('idnumber', 'groupmode', 'groupingid', + // 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($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); - } - } + '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 $mod; + return $mods; } /** @@ -814,11 +839,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. } @@ -837,7 +858,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_purge_module_cache($cm); + rebuild_course_cache($cm->course, false, true); } return ($cm->groupmode != $groupmode); } @@ -847,7 +869,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_purge_module_cache($cm); + rebuild_course_cache($cm->course, false, true); } return ($cm->idnumber != $idnumber); } @@ -924,7 +947,8 @@ function set_coursemodule_visible($id, $visible, $visibleoncoursepage = 1) { } } - rebuild_course_cache($cm->course, true); + course_purge_module_cache($cm); + rebuild_course_cache($cm->course, false, true); return true; } @@ -961,7 +985,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_purge_module_cache($cm); + 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)); @@ -1113,13 +1138,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_purge_module_cache($cm); + rebuild_course_cache($cm->course, false, true); } /** @@ -1376,17 +1402,23 @@ function move_section_to($course, $section, $destination, $ignorenumsections = f $movedsections = reorder_sections($sections, $section, $destination); + $sectioninfo = new stdClass; + $sectioninfo->course = $course->id; // Update all sections. Do this in 2 steps to avoid breaking database // 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)); + $DB->set_field('course_sections', 'section', -$position, ['id' => $id]); + $sectioninfo->section = $id; + course_purge_section_cache($sectioninfo); } } foreach ($movedsections as $id => $position) { if ($sections[$id] !== $position) { - $DB->set_field('course_sections', 'section', $position, array('id' => $id)); + $DB->set_field('course_sections', 'section', $position, ['id' => $id]); + $sectioninfo->section = $id; + course_purge_section_cache($sectioninfo); } } @@ -1394,14 +1426,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; } @@ -1576,7 +1608,8 @@ 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); + course_purge_section_cache($section); + rebuild_course_cache($courseid, false, true); course_get_format($courseid)->update_section_format_options($data); // Update fields of the $section object. @@ -1608,11 +1641,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_purge_module_cache($cm); } \core\event\course_module_updated::create_from_cm($cm)->trigger(); } } + rebuild_course_cache($courseid, false, true); } } @@ -5130,3 +5165,50 @@ function course_output_fragment_new_base_form($args) { return $o; } + +/** + * 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 + */ +function course_purge_section_cache(object $sectioninfo): void { + $section = $sectioninfo->section; + $courseid = $sectioninfo->course; + $cache = cache::make('core', 'coursemodinfo'); + $cache->acquire_lock($courseid); + $coursemodinfo = $cache->get($courseid); + if (($coursemodinfo !== false) && array_key_exists($section, $coursemodinfo->sectioncache)) { + unset($coursemodinfo->sectioncache[$section]); + $cache->set($courseid, $coursemodinfo); + } + $cache->release_lock($courseid); +} + +/** + * 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 + */ +function course_purge_module_cache($cm): void { + $cmid = $cm->id; + $courseid = $cm->course; + $cache = cache::make('core', 'coursemodinfo'); + $cache->acquire_lock($courseid); + $coursemodinfo = $cache->get($courseid); + $hascache = ($coursemodinfo !== false) && array_key_exists($cmid, $coursemodinfo->modinfo); + if ($hascache) { + unset($coursemodinfo->modinfo[$cmid]); + $cache->set($courseid, $coursemodinfo); + } + $cache->release_lock($courseid); +} diff --git a/course/mod.php b/course/mod.php index 67719a7aae2..86fc2bfcda5 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_purge_module_cache($cm); + // 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 2afb5963cea..80e24b7b614 100644 --- a/course/modlib.php +++ b/course/modlib.php @@ -374,7 +374,11 @@ function edit_module_post_actions($moduleinfo, $course) { $moduleinfo->showgradingmanagement = $showgradingmanagement; } - rebuild_course_cache($course->id, true); + $cminfo = new stdClass(); + $cminfo->course = $moduleinfo->course; + $cminfo->id = $moduleinfo->coursemodule; + course_purge_module_cache($cminfo); + 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 32f66639e07..9197e1a8ac6 100644 --- a/course/tests/courselib_test.php +++ b/course/tests/courselib_test.php @@ -1099,6 +1099,7 @@ class core_course_courselib_testcase 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 48650aa6cc7..03621e5cfe2 100644 --- a/course/tests/externallib_test.php +++ b/course/tests/externallib_test.php @@ -1085,8 +1085,9 @@ 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); + course_purge_section_cache($sectionrecord); + rebuild_course_cache($course->id, true, true); return array($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm, $forumcompleteautocm); } diff --git a/lib/db/caches.php b/lib/db/caches.php index 5fabc3bbfb3..80f6d189f60 100644 --- a/lib/db/caches.php +++ b/lib/db/caches.php @@ -221,8 +221,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_purge_module_cache() + // - course_purge_section_cache() + // - remove_course_contents(). 'coursemodinfo' => array( 'mode' => cache_store::MODE_APPLICATION, 'simplekeys' => true, diff --git a/lib/modinfolib.php b/lib/modinfolib.php index 191e5e062a6..a29078654ff 100644 --- a/lib/modinfolib.php +++ b/lib/modinfolib.php @@ -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; } } @@ -553,6 +553,7 @@ class course_modinfo { $this->instances[$cm->modname] = array(); } $this->instances[$cm->modname][$cm->instance] = $cm; + ksort($this->instances[$cm->modname]); $this->cms[$cm->id] = $cm; // Reconstruct sections. This works because modules are stored in order @@ -562,6 +563,8 @@ class course_modinfo { $this->sections[$cm->sectionnum][] = $cm->id; } + ksort($this->cms); + ksort($this->instances); // Expand section objects $this->sectioninfo = array(); foreach ($coursemodinfo->sectioncache as $number => $data) { @@ -588,23 +591,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($course->id); + 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 +632,7 @@ class course_modinfo { section_info::convert_for_section_cache($compressedsections[$number]); } + ksort($compressedsections); return $compressedsections; } @@ -652,19 +670,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 Indicates it's partial course 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 +694,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 Is partial 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"); @@ -693,8 +714,8 @@ class course_modinfo { // This may take time on large courses and it is possible that another user modifies the same course during this process. // Field cacherev stored in both DB and cache will ensure that cached data matches the current course state. $coursemodinfo = new stdClass(); - $coursemodinfo->modinfo = get_array_of_activities($course->id); - $coursemodinfo->sectioncache = self::build_course_section_cache($course); + $coursemodinfo->modinfo = get_array_of_activities($course->id, $partialrebuild); + $coursemodinfo->sectioncache = self::build_course_section_cache($course, $partialrebuild); foreach (self::$cachedfields as $key) { $coursemodinfo->$key = $course->$key; } @@ -2415,9 +2436,22 @@ 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 course_purge_module_cache() or course_purge_section_cache() must be + * called before when partialrebuild is true. + * use course_purge_module_cache() to invalidate mod cache. + * use course_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 !empty($partialrebuild)) { + $error = 'partialrebuild only works when a valid course id is provided.'; + throw new coding_exception($error); + } // Function rebuild_course_cache() can not be called during upgrade unless it's clear only. if (!$clearonly && !upgrade_ensure_not_running(true)) { @@ -2433,7 +2467,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)); @@ -2446,7 +2483,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) { @@ -2471,10 +2512,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/tests/modinfolib_test.php b/lib/tests/modinfolib_test.php index 0f6222871c2..97fa7cd76c6 100644 --- a/lib/tests/modinfolib_test.php +++ b/lib/tests/modinfolib_test.php @@ -365,7 +365,7 @@ class modinfolib_test extends advanced_testcase { $this->assertEquals($USER->id, $modinfo->userid); $this->assertEquals(array(0 => array($forum0->cmid, $assign0->cmid), 1 => array($forum1->cmid, $assign1->cmid, $page1->cmid), 3 => array($page3->cmid)), $modinfo->sections); - $this->assertEquals(array('forum', 'assign', 'page'), array_keys($modinfo->instances)); + $this->assertEquals(array('assign', 'forum', 'page'), array_keys($modinfo->instances)); $this->assertEquals(array($assign0->id, $assign1->id), array_keys($modinfo->instances['assign'])); $this->assertEquals(array($forum0->id, $forum1->id), array_keys($modinfo->instances['forum'])); $this->assertEquals(array($page1->id, $page3->id), array_keys($modinfo->instances['page'])); diff --git a/lib/upgrade.txt b/lib/upgrade.txt index 971a378ca0e..371ab500cab 100644 --- a/lib/upgrade.txt +++ b/lib/upgrade.txt @@ -25,7 +25,8 @@ 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. * 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 06abf29fc9f..e742d2dce40 100644 --- a/mod/chat/lib.php +++ b/mod/chat/lib.php @@ -713,6 +713,9 @@ function chat_update_chat_times($chatid=0) { $cond = "modulename='chat' AND eventtype = :eventtype AND instance = :chatid AND timestart <> :chattime"; $params = ['chattime' => $chat->chattime, 'eventtype' => CHAT_EVENT_TYPE_CHATTIME, 'chatid' => $chat->id]; + $cm = get_coursemodule_from_instance('chat', $chat->id, $chat->course); + course_purge_module_cache($cm); + if ($event->id = $DB->get_field_select('event', 'id', $cond, $params)) { $event->timestart = $chat->chattime; $event->timesort = $chat->chattime; @@ -723,7 +726,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 b14a4cdddb7..03dc710144a 100644 --- a/mod/lti/locallib.php +++ b/mod/lti/locallib.php @@ -2833,14 +2833,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_purge_module_cache($record); + } + $rs->close(); + $courseids = array_unique($courseids); + foreach ($courseids as $courseid) { + rebuild_course_cache($courseid, false, true); } } }