diff --git a/course/lib.php b/course/lib.php index f98e9b3f884..e088d7d229c 100644 --- a/course/lib.php +++ b/course/lib.php @@ -1376,22 +1376,28 @@ function set_section_visible($courseid, $sectionnumber, $visibility) { function get_print_section_cm_text(cm_info $cm, $course) { global $OUTPUT; - // Get course context - $coursecontext = get_context_instance(CONTEXT_COURSE, $course->id); - // Get content from modinfo if specified. Content displays either // in addition to the standard link (below), or replaces it if // the link is turned off by setting ->url to null. if (($content = $cm->get_content()) !== '') { + // Improve filter performance by preloading filter setttings for all + // activities on the course (this does nothing if called multiple + // times) + filter_preload_activities($cm->get_modinfo()); + + // Get module context + $modulecontext = get_context_instance(CONTEXT_MODULE, $cm->id); $labelformatoptions = new stdClass(); $labelformatoptions->noclean = true; $labelformatoptions->overflowdiv = true; - $labelformatoptions->context = $coursecontext; + $labelformatoptions->context = $modulecontext; $content = format_text($content, FORMAT_HTML, $labelformatoptions); } else { $content = ''; } + // Get course context + $coursecontext = get_context_instance(CONTEXT_COURSE, $course->id); $stringoptions = new stdClass; $stringoptions->context = $coursecontext; $instancename = format_string($cm->name, true, $stringoptions); diff --git a/lib/filterlib.php b/lib/filterlib.php index 73ddf0b342e..7dd5b939f31 100644 --- a/lib/filterlib.php +++ b/lib/filterlib.php @@ -816,7 +816,15 @@ function filter_get_all_local_settings($contextid) { * array('filter/tex' => array(), 'mod/glossary' => array('glossaryid', 123)) */ function filter_get_active_in_context($context) { - global $DB; + global $DB, $FILTERLIB_PRIVATE; + + // Use cache (this is a within-request cache only) if available. See + // function filter_preload_activities. + if (isset($FILTERLIB_PRIVATE->active) && + array_key_exists($context->id, $FILTERLIB_PRIVATE->active)) { + return $FILTERLIB_PRIVATE->active[$context->id]; + } + $contextids = str_replace('/', ',', trim($context->path, '/')); // The following SQL is tricky. It is explained on @@ -850,6 +858,129 @@ function filter_get_active_in_context($context) { return $filters; } +/** + * Preloads the list of active filters for all activities (modules) on the course + * using two database queries. + * @param course_modinfo $modinfo Course object from get_fast_modinfo + */ +function filter_preload_activities(course_modinfo $modinfo) { + global $DB, $FILTERLIB_PRIVATE; + + // Don't repeat preload + if (!isset($FILTERLIB_PRIVATE->preloaded)) { + $FILTERLIB_PRIVATE->preloaded = array(); + } + if (!empty($FILTERLIB_PRIVATE->preloaded[$modinfo->get_course_id()])) { + return; + } + $FILTERLIB_PRIVATE->preloaded[$modinfo->get_course_id()] = true; + + // Get contexts for all CMs + $cmcontexts = array(); + $cmcontextids = array(); + foreach ($modinfo->get_cms() as $cm) { + $modulecontext = get_context_instance(CONTEXT_MODULE, $cm->id); + $cmcontextids[] = $modulecontext->id; + $cmcontexts[] = $modulecontext; + } + + // Get course context and all other parents... + $coursecontext = get_context_instance(CONTEXT_COURSE, $modinfo->get_course_id()); + $parentcontextids = explode('/', substr($coursecontext->path, 1)); + $allcontextids = array_merge($cmcontextids, $parentcontextids); + + // Get all filter_active rows relating to all these contexts + list ($sql, $params) = $DB->get_in_or_equal($allcontextids); + $filteractives = $DB->get_records_select('filter_active', "contextid $sql", $params); + + // Get all filter_config only for the cm contexts + list ($sql, $params) = $DB->get_in_or_equal($cmcontextids); + $filterconfigs = $DB->get_records_select('filter_config', "contextid $sql", $params); + + // Note: I was a bit surprised that filter_config only works for the + // most specific context (i.e. it does not need to be checked for course + // context if we only care about CMs) however basede on code in + // filter_get_active_in_context, this does seem to be correct. + + // Build course default active list. Initially this will be an array of + // filter name => active score (where an active score >0 means it's active) + $courseactive = array(); + + // Also build list of filter_active rows below course level, by contextid + $remainingactives = array(); + + // Array lists filters that are banned at top level + $banned = array(); + + // Add any active filters in parent contexts to the array + foreach ($filteractives as $row) { + $depth = array_search($row->contextid, $parentcontextids); + if ($depth !== false) { + // Find entry + if (!array_key_exists($row->filter, $courseactive)) { + $courseactive[$row->filter] = 0; + } + // This maths copes with reading rows in any order. Turning on/off + // at site level counts 1, at next level down 4, at next level 9, + // then 16, etc. This means the deepest level always wins, except + // against the -9999 at top level. + $courseactive[$row->filter] += + ($depth + 1) * ($depth + 1) * $row->active; + + if ($row->active == TEXTFILTER_DISABLED) { + $banned[$row->filter] = true; + } + } else { + // Build list of other rows indexed by contextid + if (!array_key_exists($row->contextid, $remainingactives)) { + $remainingactives[$row->contextid] = array(); + } + $remainingactives[$row->contextid][] = $row; + } + } + + // Chuck away the ones that aren't active + foreach ($courseactive as $filter=>$score) { + if ($score <= 0) { + unset($courseactive[$filter]); + } else { + $courseactive[$filter] = array(); + } + } + + // Loop through the contexts to reconstruct filter_active lists for each + // cm on the course + if (!isset($FILTERLIB_PRIVATE->active)) { + $FILTERLIB_PRIVATE->active = array(); + } + foreach ($cmcontextids as $contextid) { + // Copy course list + $FILTERLIB_PRIVATE->active[$contextid] = $courseactive; + + // Are there any changes to the active list? + if (array_key_exists($contextid, $remainingactives)) { + foreach ($remainingactives[$contextid] as $row) { + if ($row->active > 0 && empty($banned[$row->filter])) { + // If it's marked active for specific context, add entry + // (doesn't matter if one exists already) + $FILTERLIB_PRIVATE->active[$contextid][$row->filter] = array(); + } else { + // If it's marked inactive, remove entry (doesn't matter + // if it doesn't exist) + unset($FILTERLIB_PRIVATE->active[$contextid][$row->filter]); + } + } + } + } + + // Process all config rows to add config data to these entries + foreach ($filterconfigs as $row) { + if (isset($FILTERLIB_PRIVATE->active[$row->contextid][$row->filter])) { + $FILTERLIB_PRIVATE->active[$row->contextid][$row->filter][$row->name] = $row->value; + } + } +} + /** * List all of the filters that are available in this context, and what the * local and inherited states of that filter are. diff --git a/lib/simpletest/testfilterconfig.php b/lib/simpletest/testfilterconfig.php index ccd3592700e..9f1ad0aef26 100644 --- a/lib/simpletest/testfilterconfig.php +++ b/lib/simpletest/testfilterconfig.php @@ -548,6 +548,180 @@ class filter_get_active_available_in_context_test extends UnitTestCaseUsingDatab } } +class filter_preload_activities_test extends UnitTestCaseUsingDatabase { + private $syscontext, $catcontext, $coursecontext, $activity1context, $activity2context; + + public function setUp() { + parent::setUp(); + + // Make sure accesslib has cached a sensible system context object + // before we switch to the test DB. + $this->syscontext = get_context_instance(CONTEXT_SYSTEM); + + // Create the table we need and switch to test DB. + $this->create_test_tables(array('filter_active', 'filter_config', 'context', + 'course', 'course_modules', 'modules', 'course_sections', + 'course_modules_availability', 'grade_items'), 'lib'); + $this->create_test_tables(array('label'), 'mod/label'); + $this->switch_to_test_db(); + + // Set up systcontext in the test database. + $this->syscontext->id = $this->testdb->insert_record('context', $this->syscontext); + + // Make the course + $course = (object)array( + 'shortname' => 'TEST101'); + $course->id = $this->testdb->insert_record('course', $course); + + // Set up category and course contexts + $this->catcontext = (object)array( + 'contextlevel' => CONTEXT_COURSECAT, + 'instanceid' => 1, + 'depth' => 2, + 'path' => '/1/2'); + $this->catcontext->id = $this->testdb->insert_record('context', $this->catcontext); + $this->coursecontext = (object)array( + 'contextlevel' => CONTEXT_COURSE, + 'instanceid' => $course->id, + 'depth' => 3, + 'path' => '/1/2/3'); + $this->coursecontext->id = $this->testdb->insert_record('context', $this->coursecontext); + + // Set up section + $section = (object)array( + 'course' => $course->id); + $section->id = $this->testdb->insert_record('course_sections', $section); + + // Make course-modules + $mod = (object)array( + 'name' => 'label', + 'visible' => 1); + $mod->id = $this->testdb->insert_record('modules', $mod); + $label1 = (object)array( + 'course' => $course->id, + 'intro' => 'Intro 1', + 'name' => 'Label 1'); + $label1->id = $this->testdb->insert_record('label', $label1); + $cm1 = (object)array( + 'course' => $course->id, + 'section' => $section->id, + 'module' => $mod->id, + 'instance' => $label1->id); + $cm1->id = $this->testdb->insert_record('course_modules', $cm1); + $label2 = (object)array( + 'course' => $course->id, + 'intro' => 'Intro 2', + 'name' => 'Label 2'); + $label2->id = $this->testdb->insert_record('label', $label2); + $cm2 = (object)array( + 'course' => $course->id, + 'section' => $section->id, + 'module' => $mod->id, + 'instance' => $label2->id); + $cm2->id = $this->testdb->insert_record('course_modules', $cm2); + $this->testdb->set_field('course_sections', 'sequence', + "$cm1->id,$cm2->id", array('id' => $section->id)); + + // Set up activity contexts + $this->activity1context = (object)array( + 'contextlevel' => CONTEXT_MODULE, + 'instanceid' => $cm1->id, + 'depth' => 4, + 'path' => '/1/2/3/4'); + $this->activity1context->id = + $this->testdb->insert_record('context', $this->activity1context); + $this->activity2context = (object)array( + 'contextlevel' => CONTEXT_MODULE, + 'instanceid' => $cm2->id, + 'depth' => 4, + 'path' => '/1/2/3/5'); + $this->activity2context->id = + $this->testdb->insert_record('context', $this->activity2context); + } + + private function assert_matches($modinfo) { + global $FILTERLIB_PRIVATE; + + // Use preload cache... + $FILTERLIB_PRIVATE = new stdClass; + filter_preload_activities($modinfo); + + // Get data and check no queries are made + $before = $this->testdb->perf_get_reads(); + $plfilters1 = filter_get_active_in_context($this->activity1context); + $plfilters2 = filter_get_active_in_context($this->activity2context); + $after = $this->testdb->perf_get_reads(); + $this->assertEqual($before, $after); + + // Repeat without cache and check it makes queries now + $FILTERLIB_PRIVATE = new stdClass; + $before = $this->testdb->perf_get_reads(); + $filters1 = filter_get_active_in_context($this->activity1context); + $filters2 = filter_get_active_in_context($this->activity2context); + $after = $this->testdb->perf_get_reads(); + $this->assertTrue($after > $before); + + // Check they match + $this->assertEqual($plfilters1, $filters1); + $this->assertEqual($plfilters2, $filters2); + } + + public function test_preload() { + global $FILTERLIB_PRIVATE; + + // Get course and modinfo + $course = $this->testdb->get_record('course', array('id'=>1)); + $modinfo = new course_modinfo($course, 1); + + // Note: All the tests in this function check that the result from the + // preloaded cache is the same as the result from calling the standard + // function without preloading. + + // Initially, check with no filters enabled + $this->assert_matches($modinfo); + + // Enable filter globally, check + filter_set_global_state('filter/name', TEXTFILTER_ON); + $this->assert_matches($modinfo); + + // Disable for activity 2 + filter_set_local_state('filter/name', $this->activity2context->id, TEXTFILTER_OFF); + $this->assert_matches($modinfo); + + // Disable at category + filter_set_local_state('filter/name', $this->catcontext->id, TEXTFILTER_OFF); + $this->assert_matches($modinfo); + + // Enable for activity 1 + filter_set_local_state('filter/name', $this->activity1context->id, TEXTFILTER_ON); + $this->assert_matches($modinfo); + + // Disable globally + filter_set_global_state('filter/name', TEXTFILTER_DISABLED); + $this->assert_matches($modinfo); + + // Add another 2 filters + filter_set_global_state('filter/frog', TEXTFILTER_ON); + filter_set_global_state('filter/zombie', TEXTFILTER_ON); + $this->assert_matches($modinfo); + + // Disable random one of these in each context + filter_set_local_state('filter/zombie', $this->activity1context->id, TEXTFILTER_OFF); + filter_set_local_state('filter/frog', $this->activity2context->id, TEXTFILTER_OFF); + $this->assert_matches($modinfo); + + // Now do some filter options + filter_set_local_config('filter/name', $this->activity1context->id, 'a', 'x'); + filter_set_local_config('filter/zombie', $this->activity1context->id, 'a', 'y'); + filter_set_local_config('filter/frog', $this->activity1context->id, 'a', 'z'); + // These last two don't do anything as they are not at final level but I + // thought it would be good to have that verified in test + filter_set_local_config('filter/frog', $this->coursecontext->id, 'q', 'x'); + filter_set_local_config('filter/frog', $this->catcontext->id, 'q', 'z'); + $this->assert_matches($modinfo); + } +} + class filter_delete_config_test extends UnitTestCaseUsingDatabase { protected $syscontext; @@ -659,4 +833,3 @@ class filter_filter_set_applies_to_strings extends UnitTestCaseUsingDatabase { $this->assertTrue($CFG->filterall); } } -