Create custom Django filters for Django templates

Problem

You want to incorporate functionality into Django templates via a filter that isn't available in Django's built-in filters.

Solution

You can place .py files that contain Django custom filters in one of two locations. You can place the .py file in a folder called templatetags inside any installed Django app. Or you can place the .py file anywhere in a Django project and use the libraries option of the TEMPLATES variable to register the file. To use custom filters in a Django template, you can use the {% load %} tag to gain access to the underlying file with filters or you can provide access to all filters in a file to all templates by using the builtins option of the TEMPLATES variable.

Next, in the .py files create standard Python methods to perform the filter logic and decorate the standard methods with the @register.filter() annotation to tell Django it's a custom filter. Set the parameter needs_autoescape=True on the @register.filter() annotation so the filter method output is considered 'safe'. Set the parameter name on the @register.filter() annotation to override the default name of the filter based on the method name.

How it works

Django custom filters can be placed in .py files located under any Django app in a folder called templatetags. In addition, it's also possible to place Django custom filters in .py files located in a common folder within a Django project. Listing 1 illustrates a project directory structure that exemplifies these two points.

Listing 1 - Django custom filter directory structure

+-<PROJECT_DIR_project_name>
|
+-__init__.py
+-settings.py
+-urls.py
+-wsgi.py
|
+----common----+
|              |
|              +--coffeehouse_filters.py
|
+----<app_one>---+
|        	 |
|                +-__init__.py
|                +-models.py
|                +-tests.py
|                +-views.py 
|                +-----------<templatetags>---+
|                                              |
|                                              +-__init__.py      
|                                              +-store_format_tf.py
+----<app_two>---+
	         |
                 +-__init__.py
                 +-models.py
                 +-tests.py
                 +-views.py 
                 +-----------<templatetags>---+
                                               |
               	                               +-__init__.py      
                                               +-tax_operations.py
						      		  
Note Filters/tags can be loaded into any Django template in an application

Custom filters are generally grouped into files and apps based on their functionality. However, this does not restrict the usage of custom filters to certain templates (i.e.those used in an app's views.py). You can use any custom filter on any Django template irrespective of where the source file is placed.

Listing 1 illustrates two .py files -- store_formay.tf.py and tax_operations.py -- that contain custom Django custom filters. Keep in mind you need to create the templatetags folder manually inside a Django app folder. You also need to generate an __init__.py file so Python is able to import the modules inside the templatetags directory. And in addition the app needs to be defined in Django's INSTALLED_APPS variable inside settings.py for the custom filters to work properly -- see the end of the recipe 'Set up content, understand Django urls, templates and apps' if you're unfamiliar with the concept of Django apps and the INSTALLED_APPS variable.

In listing 1 there's another .py file -- coffeehouse_filters.py -- that also contains Django custom filters. This last custom filter file is different because it's located in a generic folder called common. In order for Django to locate a custom filter file in a generic location, you must declare it as part of the libraries field in OPTIONS of the TEMPLATES variable in settings.py. See the recipe Customize Django template configuration for detailed instructions on using the libraries field.

To make use of Django custom filters in Django templates you need to use of the {% load %} tag inside a Django template, as illustrated in listing 2.

Listing 2 - Configure Django template to load custom tags and filters

{% load store_format_tf %}
{% load store_format_t tax_operations %}
{% load undercoffee from store_format_tf %}

As illustrated in listing 2 there are various ways you can use the {% load %} tag. You can make all the filters present in a custom file available to a template -- note the lack of .py in the {% load %} tag syntax -- or inclusively multiple custom files at once. And you can also selectively load certain filters using a Python like syntax filter from custom_file. Keep in mind the {% load %} tag should be declared at the top of the template.

If you find yourself using the {% load %} tag extensively, you can make custom filters available to all templates without the need of the {% load %} tag using the builtins field. The builtins field is part of OPTIONS in the TEMPLATES variable in settings.py. See the recipe Customize Django template configuration for detailed instructions on using the builtins field.

Create custom Django filters with no arguments

The simplest custom Django filter you can create requires very little knowledge of Django's internals. The only thing you need to do is create a standard Python method and decorate it with the @register.filter() annotation as illustrated in Listing 3.

Listing 3 - Django custom filter with no arguments

from django import template
register = template.Library()


@register.filter()
def boldcoffee(value):
    '''Returns input wrapped in HTML  tags'''
    return '%s' % value

Listing 3 first imports the template package and creates a register reference to decorate the filter method and tell Django to create a custom filter out of it.

By default, a filter receives the same name as the decorated method. So in this case, the boldcoffee method creates a filter by the same name. The method input value represents the input of the filter caller. The method simply returns the input value wrapped in HTML <b> tags -- which is HTML syntax for bold text. The syntax used in the return statement is a standard Python string format operation -- see: http://docs.python.org/release/2.5.2/lib/typesseq-strings.html.

To apply this custom filter in a Django template you use the syntax {{byline|boldcoffee}}. Django passes the value of the variable to the backing filter method, so if the byline variable contains the text Open since 1965! the output after applying the filter would be <b>Open since 1965!</b>.

Handle HTML characters in custom Django filters

A subtle but default behavior of custom Django filters is the output is not considered 'safe'. Although the custom filter in listing 3 outputs the content surrounded by an HTML <b> tag, by default a Django template outputs this content verbatim (i.e. you won't see the text rendered in bold, but rather <b>Open since 1965!</b>). Sometimes this is a desired behavior, but sometimes it's not.

To make the Django template render HTML characters, you can apply the built-in safe filter (e.g. {{byline|boldcoffee|safe}}) or surround the filter declaration with the built-in {% autoescape %} tag (e.g. {% autoescape off %} {{byline|boldcoffee}} {% endautoescape %} tag). However, Django filters can also be designed to make the process automatic and avoid the need for this extra filter or tag.

The Django filter annotation can be setup with the is_safe=True argument (e.g. @register.filter(is_safe=True)) to treat the custom filter's output as 'safe'. This ensures all the HTML tags and characters in the filter output are rendered. So if you apply the is_safe=True to the filter in listing 3, the output which includes the <b> tag would be rendered in bold.

This last design though assumes a filter is always applied to 'safe' content, which is not necessarily true. If the same filter is used by another person with little knowledge of how the custom filter works, it's possible he could apply the filter to 'unsafe' content.

Take for example if the variable byline contains the text Open since 1965 & serving > 1000 coffees day!. This text has the & and > characters which are considered 'unsafe'. These characters are considered 'unsafe' because they have special meaning in HTML and have the potential to mangle a page layout (e.g. if another HTML tag is accidentally left open, a browser can interpret these characters as markup, when they're actual content).

To avoid the inclusion of 'unsafe' characters, it's necessary to output the escaped version of these characters so there's no possibility of misinterpretation. The 'safe' or escaped version of the & character is &amp;, where as the 'safe' or escaped version of the > character is &gt;.

Outputting escaped characters is not a problem for Django, in fact by default all content is treated as 'unsafe' and therefore all HTML characters are escaped. As you saw for the filter in listing 3, the output is generated verbatim without the text rendered in bold, but rather <b>Open since 1965!</b>. This led to the option of applying the is_safe=True argument (e.g. @register.filter(is_safe=True)) to tell Django to treat content as 'safe' and render the <b> tag in bold.

But what happens if you want a custom filter to render the <b> tag in bold and yet escape HTML characters that may be present in the input ? You need to rely on the template user to specify which content is 'safe' and 'unsafe' and design a filter to detect these scenarios. Listing 4 shows a filter that does just this.

Listing 4 - Django custom filter that detects autoescape setting

from django import template
from django.utils.html import escape
from django.utils.safestring import mark_safe

register = template.Library()

@register.filter(needs_autoescape=True)
def smartcoffee(value, autoescape=True):
    '''Returns input wrapped in HTML  tags'''
    '''and also detects surrounding autoescape on filter (if any) and escapes '''
    if autoescape:
        value = escape(value)
    result = '%s' % value
    return mark_safe(result)

The needs_autoescape parameter and the autoescape keyword argument of the filter method allow to filter to know whether automatic escaping is in effect when the filter is called. If auto escaping is on, then value is passed through the escape method to escape all characters. Whether or not the content of value is escaped, the filter passes the final result through the mark_safe method so the HTML <b> tag is interpreted as bold in the template.

This last filter is more robust than the previous filter that uses the is_safe=True argument -- and marks everything as 'safe' -- because it can deal with 'unsafe' input, as long as the template user makes appropriate use of autoescape. Various scenarios and outputs of this custom tag are illustrated in table 1.

Table 1 - Scanarios to apply custom filters that detect safe and unsafe characters
With byline variable='Open since 1965 & serving > 1000 coffees day!'
SyntaxExplanationOutput (Note: Always in bold because filter marks <b> tag as safe)
{% autoescape on %} 
...
{{byline|smartcoffee}}
...
{% endautoescape %}
User defines autoescape on before the filter is used. The filter detects the autoescape and escapes 'unsafe' characters Open since 1965 &amp; serving &gt; 1000 coffees day!
{% autoescape off %} 
...
{{byline|smartcoffee}}
...
{% endautoescape %}
User (mistakenly) defines autoescape off before the filter is used. The filter detects the autoescape off and leaves 'unsafe' characters Open since 1965 & serving > 1000 coffees day!
...
{{byline|smartcoffee}}
...
User doesn't define any autoescape behavior. But the filter defaults to escape 'unsafe' characters Open since 1965 &amp; serving &gt; 1000 coffees day!

As you can see in table 1, even when using a more robust approach to create a custom filter to deal with HTML characters, you still need to rely on the template user to make the appropriate use of autoescape.

Create custom Django filters with arguments and an explicit name

Django custom filters can also be given an explicit name -- instead of using the default method name -- as well as support the inclusion of arguments. These two features are illustrated in the custom filter in listing 5.

Listing 5 - Django custom filter with arguments and custom name

@register.filter(name='customcoffee',is_safe=True)
def coffee(value,arg="muted"): 
    '''Returns input wrapped in HTML  tags with a CSS class'''
    '''Defaults to CSS class 'muted' from Bootstrap'''
    return '%s' % (arg,value)

If you specify the name attribute in the @register annotation, you override the default naming behavior based on the filter method name. In listing 5, the filter is registered with the customcoffee name. Note that if you try to call the filter with the method name when you use the name attribute, you'll get an error because the filter doesn't exist.

The filter method in listing 5 has two input arguments. The value argument that represents the variable on which the filter is applied and a second argument arg="muted" where "muted" represents a default value for the arg variable.

If you look at the return statement in listing 5 you'll notice it uses the arg variable to define a class attribute and the value variable is used to define the content inside a <span> tag.

If you call the custom filter in listing 5 with the same syntax as the first custom filter (e.g. {{byline|customcoffee}}) the output defaults to using the "muted" value for the arg variable (e.g. <span class="black">Open since 1965!</span>).

However, you can also call the filter in listing 5 using a parameter to override the arg variable. Parameters filters are appended to the filter with:. For example, the filter statement {{byline|customcoffee:"lead muted"}} assigns "lead muted" as the value for the arg variable and produces an output like <span class="lead muted">Open since 1965!</span>.

Parameters provide more flexibility for custom filters because they can further influence the final output with data that's different from the main input.

Note To support more than two filter argements, split the values inside the custom filter method

In case a filter requires two or more arguments, you can use a space-separated or CSV-type string parameter in the filter definition (e.g.byline|mymultifilter:"18,success,green,2em"). And then you can parse the string inside the filter method to access each parameter.