Permalink
Browse files

MDL-29236 Labels (and other content from modules) on course page shou…

…ld use module filter settings

E.g. if you disable smileys for a label, this ought to work (it doesn't previously, as everything is filtered using the course context).
  • Loading branch information...
1 parent 5d2db8a commit 371fbe1cc1a49b933570958edb90e02dba801130 @sammarshallou sammarshallou committed Sep 6, 2011
Showing with 316 additions and 6 deletions.
  1. +10 −4 course/lib.php
  2. +132 −1 lib/filterlib.php
  3. +174 −1 lib/simpletest/testfilterconfig.php
View
@@ -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);
View
@@ -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.
@@ -548,6 +548,180 @@ public function test_available_in_context_exception_with_syscontext() {
}
}
+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 @@ public function test_unset_multi() {
$this->assertTrue($CFG->filterall);
}
}
-

0 comments on commit 371fbe1

Please sign in to comment.