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-insafe
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 filteris_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:
Inside apps .- Stored in .py files located inside Django apps in a folder called
templatetags
.Any project location.- Stored in .py files on any folder in a Django project, configured through the
libraries
field inOPTIONS
of theTEMPLATES
variable insettings.py
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.