Use built-in and custom filters & tests on Jinja templates (like Django filters)

Problem

You want to format or evaluate Jinja template variables to display them in a different manner than how they're passed by Django view methods. Jinja offers filters and tests to format numbers, strings, special characters, among other things.

Solution

Use one of Jinja's many built-in filters and tests. Jinja offers built-in filters and tests for several purposes that are available by default on all Jinja templates. In addition, it's also possible to transform regular Python methods into custom Jinja filters and tests.

Note Jinja filters and tests are like Django filters

The Jinja documentation makes an explicit difference between what it calls filters and tests. The only difference is Jinja tests are used to evaluate a condition vs. Jinja filters which are used to format or transform values. In Django there is no such naming difference and an equivalent Jinja test in Django is simply called a Django filter.

How it works

Jinja filters and tests are designed to format and evaluate individual template variables, respectively. The syntax to apply Jinja filters to template variables is a 'pipe' represented by the vertical bar character |. The syntax to apply a Jinja filter is the following {{variable|filter}}. It's also worth mentioning you can apply multiple filters to the same variable (e.g.{{variable|filter|filter}}). The syntax to apply Jinja tests uses the is keyword along with a regular conditional to evaluate the validity of a test: {% if variable is divisibleby 10 %}do something{% endif %}.

In the upcoming sections, I'll add the reference (Test) to indicate it's referring to a Jinja test vs. a Jinja filter. I'll also classify each Django built-in filter & test into functional sections so they are easier to identify. I'll define the broadest category 'Strings, lists, dictionaries, numbers and objects' for filters and tests that are applicable for most scenarios and then more specialized sections for each data type, including: 'String and lists', 'Dictionaries and objects', Strings, Numbers, Spacing and special characters, Development and testing and Urls. The final sections conclude with how to create and register custom filters & tests.

Strings, lists, dictionaries, numbers and objects

Listing 1 - Jinja defined test

{% if variable is defined %}
    value of variable: {{ variable }}
{% else %}
    variable is not defined
{% endif %}

Strings and lists

Dictionaries and objects

Listing 2 - Jinja groupby filter

# Dictionary definition
stores = [
    {'name': 'Downtown', 'street': '385 Main Street', 'city': 'San Diego'},
    {'name': 'Uptown', 'street': '231 Highland Avenue', 'city': 'San Diego'},
    {'name': 'Midtown', 'street': '85 Balboa Street', 'city': 'San Diego'},
    {'name': 'Downtown', 'street': '639 Spring Street', 'city': 'Los Angeles'},
    {'name': 'Midtown', 'street': '1407 Broadway Street', 'city': 'Los Angeles'},
    {'name': 'Downton', 'street': '50 1st Street', 'city': 'San Francisco'},
]

<ul>
{% for group in stores|groupby('city') %}
    <li>{{ group.grouper }}
    <ul>
        {% for item in group.list %}
          <li>{{ item.name }}: {{ item.street }}</li>
        {% endfor %}
    </ul>
    </li>
{% endfor %}
</ul>

# Output
Los Angeles
    Downtown: 639 Spring Street
    Midtown: 1407 Broadway Street
San Diego
    Downtown : 385 Main Street
    Uptown : 231 Highland Avenue
    Midtown : 85 Balboa Street
San Francisco
    Downtown: 50 1st Street

# Alternate shortcut syntax, produces same output
<ul>
{% for grouper, list in stores|groupby('city') %}
    <li>{{ grouper }}
    <ul>
        {% for item in list %}
          <li>{{ item.name }}: {{ item.street }}</li>
        {% endfor %}
    </ul>
    </li>
{% endfor %}
</ul>

Strings

Numbers

Spacing and special characters

Listing 3 - Jinja wordwrap filter

# Variable definition 
Coffeehouse started as a small store

# Template definition with wordwrap filter for every 12 characters
{{variable|wordwrap(12)}}

# Output
Coffeehouse 
started as a
small store
Listing 4 - Django xmlattr filter

# Variable definition 
{% set stores = [
    {'id':123,'name': 'Downtown', 'street': '385 Main Street', 'city': 'San Diego'},
    {'id':243,'name': 'Uptown', 'street': '231 Highland Avenue', 'city': 'San Diego'},
    {'id':357,'name': 'Midtown', 'street': '85 Balboa Street', 'city': 'San Diego'},
    {'id':478,'name': 'Downtown', 'street': '639 Spring Street', 'city': 'Los Angeles'},
    {'id':529,'name': 'Midtown', 'street': '1407 Broadway Street', 'city': 'Los Angeles'},
    {'id':653,'name': 'Downton', 'street': '50 1st Street', 'city': 'San Francisco'},
] %}

# Template definition
>ul>
{% for store in stores %}
  >li {{ {'id':'%d'|format(store.id),'class':'%s'|format(store.city|lower|replace(' ','-')) }|xmlattr }}> {{store.city}} {{store.name}}>/li>
{% endfor %}
>/ul>


# Output
>ul>
  >li id="123" class="san-diego"> San Diego Downtown>/li>
  >li id="243" class="san-diego"> San Diego Uptown>/li>
  >li id="357" class="san-diego"> San Diego Midtown>/li>
  >li id="478" class="los-angeles"> Los Angeles Downtown>/li>
  >li id="529" class="los-angeles"> Los Angeles Midtown>/li>
  >li id="653" class="san-francisco"> San Francisco Downton>/li>
>/ul>

Development and testing

Urls

Create 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. 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;). 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 5.

Listing 5 illustrates various backing methods for custom Jinja filters and tests.

Listing 5 - 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 5 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 5 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 5 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.

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 the final section in this recipe. Note that it's assumed the backing methods in listing 5 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 6 illustrates a directory structure with additional Django project files for reference.

Listing 6 - Directory structure and location of custom Jinja filters and tests
 
+---+-<PROJECT_DIR_coffeehouse>
    |
    +-__init__.py
    +-settings.py
    +-urls.py
    +-wsgi.py
    |
    +-jinja-+
            +-__init__.py
            +-env.py
            +-filters.py

Register custom filters and tests in Jinja

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

Listing 7 - 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 7, 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 7 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 this 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 8.

Listing 8 - Configure custom Jinja environment in Django setttings.py

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

In this case, you can see in listing 8 the environment value corresponds to coffeehouse.jinja.env.JinjaEnvironment, where JinjaEnvironment is the class -- in listing 7 -- 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 6.

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 rest of this recipe.