Custom filters

On occasions, Django built-in filters fall short in terms of the logic or output they offer. In these circumstances, the solution is to write a custom filter to achieve the outcome you require.

The logic behind Django filters is entirely written in Python, so whatever is achievable with Python & Django (e.g. perform a database query, use a third party REST service) can be integrated as part of the logic or output generated by a custom filter.

Structure

The simplest custom Django filter only requires you to create a standard Python method and decorate it with @register.filter() as illustrated in Listing 3-23.

Listing 3-23 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 '<b>%s</b>' % value

Listing 3-23 first imports the template package and creates a register reference to decorate the boldcoffee 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 named boldcoffee. The method input value represents the input of the filter caller. In this case, the method simply returns the input value wrapped in HTML <b> tags, where the syntax used in the return statement is a standard Python string format operation.

To apply this custom filter in a Django template you use the syntax {{byline|boldcoffee}}. The byline variable is passed as the value argument to the filter method, so if the byline variable contains the text Open since 1965! the filter output is <b>Open since 1965!</b>.

Django custom filters also support the inclusion of arguments, as illustrated in listing 3-24.

Listing 3-24. Django custom filter with arguments

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

The filter method in listing 3-24 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. If you look at the return statement 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 3-24 with the same syntax as the first custom filter (e.g. {{byline|coffee}}) the output defaults to using "muted" for the arg variable and the final output is <span class="muted">Open since 1965!</span>.

However, you can also call the filter in listing 3-24 using a parameter to override the arg variable. Filter parameters are appended with :. For example, the filter statement {{byline|coffee:"lead muted"}} assigns "lead muted" as the value for the arg variable and produces the output <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 than the main input.

Tip 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 later parse the string inside the filter method to access each parameter.

Options: Naming, HTML & what comes in and out

Although the two previous examples illustrate the core structure of custom filters, they are missing a series of options that make custom filters more flexible and powerful. Table 3-5 illustrates a series of custom filter options, along with their syntax and a description of what it's they do.

Table 3-5. Custom filter options.

Option syntax Values Description
@register.filter(name=<method_name>) A sting to name the filter Assigns a filter name different from the filter method name
@register.filter(is_safe=False) True/False Defines how to treat a filter's return value (safe or with auto-escape)
@register.filter(needs_autoescape=False) True/False Defines the need to access the auto-escaping status of the caller (i.e. whether the filter is called in a template with or without auto-escaping)
@register.filter(expects_localtime=False) True/False If the filter is applied on a datetime value, it converts the value to the project timezone, before running the filter logic.
@register.filter()
@stringfilter
N/A Standalone decorator that casts input to string

As you can see in table 3-5, with the exception of one option, all custom filter options are offered by arguments of the @register.filter() decorator and include default values. So even if you declare an empty @register.filter() decorator, four out of five options in table 3-5 operate with default values. Note it's possible to add multiple options to the @register.filter() decorator separated by commas (e.g. @register.filter(name='myfilter',is_safe=True)).

Let's talk about the name option in table 3-5. By default and as you learned in the previous examples, custom filters receive the same name as the method they decorate (i.e. if the backing method of a custom filter is named coffee, the filter is also called coffee). The name option allows you to give a filter a different name than the backing method name. Note that if use the name option and try to call the filter with the method name, you'll get an error because the filter doesn't exist by method name anymore.

All custom filters operate on input provided by variables that can potentially be any Python type (string, integer, datetime, list, dictionary,etc). This creates a multitude of possibilities that must be handled in the logic of a custom filter, otherwise errors are bound to be common (e.g. a call is made to a filter with an integer variable, but the internal filter logic is designed for string variables). To alleviate these potential input type issues, custom filters can use the last two options presented in table 3-5.

The expects_localtime option in table 3-5 is designed for filters that operate on datetime variables. If you expect a datetime input, you can set the expects_localtime to True and this makes the datetime input timezone aware based on your project settings.

The @stringfilter option in table 3-5 -- which is a standalone decorator, placed below the @register.filter decorator -- is designed to cast a filter input variable to a string. This is helpful because it removes the need to perform input type checks and irrespective of what variable type a filter is called with (e.g. string, integer, list or dictionary variable) the filter logic can ensure it will always gets a string.

A subtle but default behavior of custom filters is the output is not considered safe, due to the is_safe option in table 3-5 defaulting to False.

This default setting causes the custom filters from listings 3-23 & 3-24 that contain HTML <b> or <span> tags to create verbatim output (i.e. you won't see the text rendered in bold, but rather <b>Open since 1965!</b> literally). Sometimes this is desired behavior, but sometimes it's not.

Tip To make a Django template render HTML characters after applying a custom filter with default settings , you can use the built-in safe filter (e.g. {{byline|coffee|safe}}) or surround the filter declaration with the built-in {% autoescape %} tag (e.g. {% autoescape off %} {{byline|coffee}} {% endautoescape %} tag). However, Django filters can also set the filter is_safe option in table 3-5 to True to make the process automatic and avoid the need to use an extra filter or tag.

You can set the is_safe option in a custom filter to True, to ensure the custom filter output is rendered 'as is' (e.g. the <b> tag is rendered in bold) and HTML elements aren't escaped .

This filter design approach though makes one big assumption: a custom filter will always be called with variables containing safe content. What happens if the byline variable contains the text Open since 1965 & serving > 1000 coffees day!. The variable now contains the unsafe characters & and > , why are they unsafe ? Because they have special meaning in HTML and have the potential to mangle a page layout if they're not escaped (e.g. the > might mean 'more than' in this context, but in HTML it also means a tag opening, which a browser can interpret as markup, in turn mangling the page because it's never closed).

To avoid this potential issue of marking unsafe input characters and marking them as safe on output, you need to rely on the calling template telling the filter if the input is safe or unsafe, which takes us to the last custom filter option in table 3-5: needs_autoescape.

The needs_autoescape option -- which defaults to False -- is used to enable a filter to be informed of the underlying auto-escaping setting in the template where the filter is called. Listing 3-25 shows a filter that makes use of this option.

Listing 3-25. 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 = '<b>%s</b>' % value
    return mark_safe(result)

The needs_autoescape parameter and the autoescape keyword argument of the filter method allow the filter to know whether escaping is in effect when the filter is called. If auto-escaping is on, then the 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 filter is more robust than a filter that uses the is_safe=True option -- and marks everything as 'safe' -- because it can deal with unsafe input, as long as the template user makes the appropriate use of auto-escape.

Installation and access

Django custom filters can be stored in one of two locations:

Listing 3-26 illustrates a project directory structure that exemplifies these two locations to store custom filters.

Listing 3-26. Django custom filter directory structure

+-<PROJECT_DIR_project_name>
|
+-asgi.py
+-__init__.py
+-settings.py
+-urls.py
+-wsgi.py
|
+----common----+
|              |
|              +--coffeehouse_filters.py
|
+----<app_one>---+
|                |
|                +-admin.py
|                +-apps.py
|                +-__init__.py
|                +-<migrations>
|                +-models.py
|                +-tests.py
|                +-views.py 
|                +-----------<templatetags>---+
|                                             |
|                                             +-__init__.py      
|                                             +-store_format_tf.py
+----<app_two>---+
                 |
                 +-admin.py
                 +-apps.py
                 +-__init__.py
                 +-<migrations>
                 +-models.py
                 +-tests.py
                 +-views.py 
                 +-----------<templatetags>---+
                                              |
                                              +-__init__.py      
                                              +-tax_operations.py

Listing 3-26 shows two apps that contain Django custom filters in two different files -- store_formay.tf.py and tax_operations.py. Keep in mind you need to create the templatetags folder manually inside a Django app folder and also create an __init__.py file so Python is able to import the modules from this folder. In addition, remember apps need to be defined in Django's INSTALLED_APPS variable inside settings.py for the custom filters to be loaded.

In listing 3-26 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 first section in this chapter for detailed instructions on using the libraries field.

Even though custom filters are generally placed into files and apps based on their functionality, this does not restrict the usage of custom filters to certain templates. You can use custom filters on any Django template irrespective of where custom filter are stored.

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

Listing 3-27 Configure Django template to load custom filters

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

As shown in listing 3-27 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. In addition, you can also selectively load certain filters using the Python like syntax load filter from custom_file. Keep in mind the {% load %} tag should be declared at the top of the template.

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