Note:

If you want to create a new page for developers, you should create it on the Moodle Developer Resource site.

Filter enable/disable by context

From MoodleDocs

Moodle 2.0


We want to make configuration of filters more flexible, so, for example, you can do things like

  • Disable the glossary auto-linking filter in a quiz.
  • Enable the TeX filter only in the Maths course category.
  • Have the glossary auto-linking filter link words from glossary A in forum A, and from glossary B in forum B.

Please discuss this proposal in this General Developer Forum thread.

Overview

Most filter settings will still be controlled at the system level. Administrators will still choose which filters enabled for the whole site, and, more importantly, which ones are not available at all. Also, most filter settings (for example which media types for the multimedia filter, and the paths for the TeX filter) will also still be set globally for the whole site by the administrator.

However, in addition to the overall on/off switch for each filter, we will store a separate active/inactive state for each filter in each context. (Don't worry, we don't actually store a row in the database for each context, mostly we use inheritance.) It will also be possible for a filter to have a settings screen for each context, and offer per-context settings, if it chooses to.

This proposal relies on some of the proposed navigation changes for Moodle 2.0, to give us a place to add a new 'Text filtering' setting link for each context.

Detailed design

Changes to the filter admin screen

On Administration ► Plugins ► Filters ► Manage filters, the existing Disable/Enable column will be changed to a column called Enabled?. For each filter there will be a dropdown with three choices:

  • Disabled - not used at all on this site.
  • Off, but available - available for use, but not turned on in the system context.
  • On - available, and on in the system context, and elsewhere by default.

(A filter may need to be enabled, but not active globally, for example in the "TeX filter only in the maths course" use case.)

We will also add a Delete column to this table, to let administrators delete all the related configuration from the database if they uninstall a plugin.

Add UI for $CFG->stringfilters

At the moment, this setting is used in code, but there is no way to define it other than by writing directly to the config table in the database. We should add UI to it, probably at site level.

UI mockup on MDL-7336. Note, we will set both $CFG->filterallstrings and $CFG->stringfilters from the Apply to dropdowns. There will be now way to override those locally, but $CFG->stringfilters will be merged will the list of enabled filters in the context.

New settings page in each context

There will be a "Text filtering" settings page for each context (apart from block contexts), just like there are Assign roles and Override permissions pages.

The page will have a title something like "Text filtering in Course: Maths 101". It will contain a table with one row for each filter that the administrator has enabled. For each filter, there will be a drop-down with three choices: Default, On and Off. For the default choice, it will display the value that would be inherited in brackets, for example "Default (on)". There will be a Save changes button at the bottom.

Per-context filter settings page.png

Make possible per-filter settings page in each context

Filters may create a file filterlocal.php in their plugin folder, and define a function has_local_config() in their filter.php file that returns true, if they want to offer different configuration options in different contexts.

This configuration is stored in in the filter_config table (see below).

Filter configuration does not inherit, and even if a filter has local configuration, it must be able to operate (perhaps by just doing nothing, or doing something default) if the local configuration variables are not set in the database.

(For example, the Glossary auto-linking filter might link to words from the course's primary glossary by default, but have a local settings page where a teacher can choose to link words from another glossary in this context.)

New capability moodle/filter:manage

Access to the global filter settings will still be controlled by moodle/site:config, becuase this is potentially risky.

Access to local override settings will be controlled by a new setting moodle/filter:manage. This capability has no associated risks, assuming that the admin has been sensible.

By default Administrators, Course creators and Teachers will have teh moodle/filter:manage capability.

New database table filter_active

This table replaces $CFG->textfilters.

Column Type Comment
id INT(10) AUTO-INCREMENT Unique id
filter VARCHAR(32) e.g. 'filter/tex' or 'mod/glossary'
contextid INT(10) Foreign key references context.id
active INT(4) 1 = active, -1 = inactive. As a special case, if contextid == $systemcontext->id, then -9999 is used to mean that the administrator has disabled this filter.
sortorder INT(10) This is only used when contextid == $systemcontext->id. It stores the filter sort-order, numbering from 1. In other contexts, this column should contain 0.

Whenever a context is deleted, we must delete the corresponding rows from this table.

We will not delete data from here when a filter is disabled globally, so that if the administrator disables then re-enables the filter, the setting in all the different contexts are not lost.

New database table filter_config

This stores filter configuration, as a set of name->value pairs, for each filter in each context. (By default, there will be 0 rows per filter per context.)

Column Type Comment
id INT(10) AUTO-INCREMENT Unique id
filter VARCHAR(32) e.g. 'filter/tex' or 'mod/glossary'
contextid INT(10) Foreign key references context.id
name VARCHAR(255) The name of a setting variable
value TEXT The corresponding value

Whenever a context is deleted, we must delete the corresponding rows from this table.

We will not delete data from here when a filter is disabled globally, so that if the administrator disables then re-enables the filter, the setting in all the different contexts are not lost.

Retrieving the active filters for a page

One of the Navigation 2.0 changes is that each page will be linked to a specific context. (That is, $PAGE->context will always to set to something sensible.)

Text will be filtered according to the page it appears on, not the context it belongs to. This is normally fine. For example, we want question text to be filtered according to the quiz that the question appears in.

With that in mind, suppose $contextids is the list of ids of this context and all its parents. Then the list of active filters is:

SELECT f.filter
FROM filter_active f
JOIN context ctx ON f.contextid = ctx.id
WHERE ctx.id IN ($contextids)
GROUP BY filter
HAVING MAX(f.active * ctx.depth) > -MIN(f.active * ctx.depth)
ORDER BY MAX(f.sortorder)

Why does this work?

To understand what that query is doing, you need to look at the results of

SELECT f.filter, f.contextid, ctx.depth, f.active, ctx.depth * f.active AS active_x_depth, f.sortorder
FROM filter_active f
JOIN context ctx ON f.contextid = ctx.id
WHERE ctx.id IN ($contextids)
ORDER BY f.filter, ctx.depth

That shows the data that is being aggregated by the GROUP BY and HAVING clauses. I will explain with reference to a specific example:

Suppose we are in a Quiz in the Maths 101 course in the Maths category in the system. $contextids = 1,3,10,22 (order reversed relative to the previous sentence - system context first). Then suppose that the above query returns

filter contextid depth active active_x_depth f.sortorder
filter/mediaplugin 1 1 1 1 1
filter/multilang 1 1 -1 -1 1
filter/tex 1 1 -1 -1 3
filter/tex 3 2 1 2 0
filter/tidy 1 1 -9999 -9999 4
filter/tidy 22 4 1 4 0
mod/glossary 1 1 1 1 2
mod/glossary 3 2 -1 -2 0
mod/glossary 10 3 1 3 0
mod/glossary 22 4 -1 -4 0
filter/mediaplugin
This is enabled, and active globally, and there are no overrides.
  • MAX(f.active * ctx.depth) = 1
  • MIN(f.active * ctx.depth) = 1
1 > -1, so this filter will be returned by the get active filters query.
filter/multilang
This is enabled, but not active globally, and there are no overrides.
  • MAX(f.active * ctx.depth) = -1
  • MIN(f.active * ctx.depth) = -1
-1 ≯ 1, so this filter will not be returned by the get active filters query.
filter/tex
This is enabled, but not active globally. However, it has been activated in the Maths category.
  • MAX(f.active * ctx.depth) = 2
  • MIN(f.active * ctx.depth) = -1
2 > 1, so this filter will be returned by the get active filters query.
filter/tidy
This is not enabled, but there is an old override in the database
  • MAX(f.active * ctx.depth) = 4
  • MIN(f.active * ctx.depth) = -9999
4 ≯ 9999, so this filter will not be returned by the get active filters query.
filter/glossary
A slightly silly example with the maximum number of overrides possible. The most specific override is the one that says that the filter is not active in the quiz.
  • MAX(f.active * ctx.depth) = 3
  • MIN(f.active * ctx.depth) = -4
3 ≯ 4, so this filter will not be returned by the get active filters query.
filter/myfilter
What! there is no filter/myfilter in the data above. What is going on? This is the situation when the administrator has just installed the myfilter plugin, but has not yet been to the manage filters administration page to update the filter settings. What happens?
Since there is nothing in the database, this filter is not returned by the get active filters query. That is, a newly installed filter is not active until the administrator explicitly activates it.

Sort order: Note that sortorder is zero except when contextid = 1. So MAX(f.sortorder) is just a clever trick to pull out the sortorder from the system context, ignoring all the zeros. There are other tricks that could be used instead, but this one is probably quite efficient.

Getting filter configuration

Actually, we will get the filter configuration in the same database query that we get the list of active filters. So the actual query used will be:

SELECT active.filter, fc.name, fc.value
FROM (SELECT f.filter
    FROM filter_active f
    JOIN context ctx ON f.contextid = ctx.id
    WHERE ctx.id IN ($contextids)
    GROUP BY filter
    HAVING MAX(f.active * ctx.depth) > -MIN(f.active * ctx.depth)
    ORDER BY MAX(f.sortorder)) active
LEFT JOIN filter_config fc ON fc.filter = active.filter AND fc.contextid = $contextid

This will be done in a function get_active_filters that looks like:

/**
 * Get the list of active filters, in the order that they should be used
 * for a particular context.
 *
 * @param object $context a context
 *
 * @return array an array where the keys are the filter names, for example
 *      'filter/tex' or 'mod/glossary' and the values are any local
 *      configuration for that filter, as an array of name => value pairs
 *      from the filter_config table. In a lot of cases, this will be an
 *      empty array. So, an example return value for this function might be
 *      array('filter/tex' => array(), 'mod/glossary' => array('glossaryid', 123))
 */
function get_active_filters($context) {
    // ...
}

Changes to text caching

The md5key used in the cache_text table will be changed to user the contextid, rather than the courseid, in $hashstr. See format_text in weblib.php.

Backup and restore

This will be similar in some ways to the roles backup and restore code, in that it needs to do work for every context in the backup file.

On backup (in lib/backuplib.php), everywhere it does write_role_overrides_xml and write_role_assignments_xml we need to add a call to a new function write_filter_actives_xml. (I think we need a write_context_data_xml function to avoid duplicating the calls to those three functions in lots of places.) That function can write XML like

<FILTERACTIVES>
    <FILTERACTIVE>
        <FILTER>filter/tex</FILTER>
        <ACTIVE>+1</ACTIVE>
    <FILTERACTIVE>
    <FILTERACTIVE>
        <FILTER>mod/glossary</FILTER>
        <ACTIVE>-1</ACTIVE>                
    <FILTERACTIVE>
</FILTERACTIVES>
<FILTERCONFIGS>
    <FILTERCONFIG>
        <FILTER>filter/tex</FILTER>
        <NAME>glossaryid</NAME>
        <VALUE>123</VALUE>
    <FILTERCONFIG>
<FILTERCONFIGS>

In the function restore_execute in backup/restorelib.php, probably just after the bit that calls restore_roles_settings, add a call to restore_filter_actives. This needs to read each FILTERACTIVE record from the backup file, check whether the corresponding filters in installed on this Moodle site, and if it is, write the record to the filter_active table using the recoded contextid.

Possible problems

Is filtering by page context really the right thing?

Filtering according to page context might cause strange results on pages that aggregate content from different contexts.

For example, we normally think that the user's profile page is in the user context. That would mean that the Recent activity report there will be filtered according to the filter settings for the user context. (These will almost certainly inherit unmodified from the System context.) That would be strange if the TeX filter was only enabled in the Maths course. However, it may work better (both here and elsewhere) if we set $PAGE->context to the course context for profile pages like this.

On the other hand, if the parent looking at their child's recent activity does not have access to the course, it is good that they do not see the effects of the glossary auto-linking filter, which would like them to glossary entries they do not have permission to see.

The real problem page, would, of course, be the My Moodle page. That could aggregate content from any number of different contexts.

Or, we may find that we have to add a $contextid parameter to the format_text function, and find some way to avoid the performance problems that causes. (It would be really bad to query the database with a query like the one above for each call to format_text. Or even for each context that contributes content to the My Moodle page.)

At the moment, the plan is to ignore this problem. This proposal adds a significant new functionality, and this one problem does not stop it from being very useful in most cases.

See also