Custom filters & tests in Jinja

Custom Jinja filters and tests are easy to create because they are backed by regular Python methods. For custom Jinja filters a method should return the desired formatted value and for Jinja tests a method should contain the logic to return a boolean value.

Structure

The backing method for a custom Jinja filter or test has arguments that correspond to the variable itself -- as the first argument -- and any remaining values passed by the filter or test as other method arguments (e.g. variable|mycustomfilter("<div>") backed by a method like def mycustomfilter(variable,htmltag="<p>"): -- note the htmltag argument has a default value in case the filter doesn't specify this value with a statement like variable|mycustomfilter).

The only Jinja specific logic you need to consider when creating backing Python methods is related to character safety in Jinja filters. By default, if a backing method for a Jinja filter returns a string it's considered unsafe and is therefore escaped (e.g. if the result of the filter returns <div>, Jinja renders the result as &lt;div&gt;) -- which is the same behavior enforce by custom Django filters.

To mark the result as a safe string, the backing Python method used by a Jinja filter must return a jinja2.Markup type, a process that's illustrated in one of the sample filters in listing 4-23. Listing 4-23. illustrates various backing methods for custom Jinja filters and tests.

Listing 4-23 Backing Python methods for Jinja custom filters and tests.

import jinja2

def customcoffee(value,arg="muted"):
    return jinja2.Markup('%s' % (arg,value))

import math

def squarerootintext(value):
    return "The square root of %s is %s" % (value,math.sqrt(value))

def startswithvowel(value):
    if value.lower().startswith(("a", "e", "i", "o","u")):
        return True
    else:
        return False

The first method in listing 4-23 returns the value argument wrapped in an HTML <span> tag and appends a CSS class with the arg argument -- note the arg argument in the method definition defaults to the muted value in case no value is provided. And also notice the customcoffee method returns a jinja2.Markup type, this is done so Jinja renders the output as a safe string and interprets the <span> tag as HTML.

The second method in listing 4-23 calculates the square root of a given value and returns the standard string "The square root of %s is %s" where the first %s represents the passed in value and the second %s the calculated square root. The third method in listing 4-23 takes the value argument, transforms it to lower case and checks if value starts with a vowel, if it does it returns a boolean True otherwise it returns a boolean False.

Installation and access

Once you create the backing methods for custom Jinja filters and tests, you need to declare them as part of the filters and/or tests variables on Jinja's environment configuration, which is described in the next section. Note that it's assumed the backing methods in listing 4-23 are placed in a file/module named filters under the coffeehouse.jinja directory path.

To better illustrate the location of the filters.py file containing the custom Jinja extension, listing 4-24 illustrates a directory structure with additional Django project files for reference.

Listing 4-24 Directory structure and location of custom Jinja filters and tests

+---+-<PROJECT_DIR_coffeehouse>
    |
    +-asgi.py
    +-__init__.py
    +-settings.py
    +-urls.py
    +-wsgi.py
    |
    +-jinja-+
            +-__init__.py
            +-env.py
            +-filters.py

Jinja filters and tests are set up as part of Jinja's environment configuration. Listing 4-25 illustrates a custom Jinja environment definition that sets a series of custom Jinja filters through the variable named filters and tests.

Listing 4-25 Custom Jinja environment with custom filters and tests

from jinja2.environment import Environment
from coffeehouse.jinja.filters import customcoffee, squarerootintext, startswithvowel

class JinjaEnvironment(Environment):

    def __init__(self,**kwargs):
        super(JinjaEnvironment, self).__init__(**kwargs)
        self.filters['customcoffee'] = customcoffee
        self.filters['squarerootintext'] = squarerootintext
        self.filters['startswithvowel'] = startswithvowel
        self.tests['startswithvowel'] = startswithvowel

As you can see in listing 4-25, each backing Python method is first imported into the custom Jinja environment. Next, to register custom Jinja filters you access self.filters and assign it a variable key name -- corresponding to the filter name -- along with the backing method for the filter. And to register custom Jinja tests you access self.tests and assign it a variable key name -- corresponding to the test name -- along with the backing method for the test.

An interesting aspect of listing 4-25 is the registration of startswithvowel as both a filter and test, which means the same backing method -- which returns True or False -- can be used for both cases. This dual registration allows startswithvowel to either use a pipe (i.e. as a filter {{variable|startwithvowel}} to output True or False verbatim) or the is keyword in a conditional (i.e. as a test {% if variable is startswithvowel %}variable starts with vowel{% endif %}).

Once a custom Jinja environment is created, you need to set it up as part of Django's configuration in the OPTIONS variable of settings.py, as illustrated in listing 4-26.

Listing 4-26 Configure custom Jinja environment in Django setttings.py

TEMPLATES = [
    { 
        'BACKEND':'django.template.backends.jinja2.Jinja2',
	'DIRS': [ PROJECT_DIR / 'jinjatemplates' ],
        'APP_DIRS': True,
        'OPTIONS': { 
            'environment': 'coffeehouse.jinja.env.JinjaEnvironment'
            }
        },
    ]

In this case, you can see in listing 4-26 the environment value corresponds to coffeehouse.jinja.env.JinjaEnvironment, where JinjaEnvironment is the class -- in listing 4-25 -- env is the file/module name and coffeehouse.jinja is the directory path. To better illustrate the location of the env.py file take a look at the directory structure in listing 4-24.

Once you finish this last registration step, all the declared custom Jinja filters and tests become available on all Jinja templates just like the regular built-in filters & tags described in the previous section.

Note The custom Jinja environment in listing 4-25 for custom Jinja filters and tests, is the same technique used in listing 4-11 to declare Jinja globals.