Use and create extensions on Jinja templates (like Django template library tags)

Problem

You want to use third-party Jinja template extensions to get access to new functionality (e.g. Django-like template tags) or you want to create your own custom Jinja template extensions to use complex template logic with the use of a single statement.

Solution

To use Jinja extensions in a Django project you need to declare them as part of the extensions key of the OPTIONS variable, which itself is part of Jinja's TEMPLATES Django configuration in settings.py.

To create a custom Jinja extension you need to re-use the functionality provided by Jinja's jinja2.ext.Extension class, as well as use Jinja's API to create the custom logic you're pursuing. Once you create a custom Jinja extension and add it to your Django project, you must also add it to the extensions key of the OPTIONS variable in settings.py.

How it works

A Jinja extension is to Jinja templates, what a library is for programming languages: a re-usable set of features contained in a specific format to not have to continuously 'reinvent the wheel'.

Jinja itself includes various extensions that need to be enabled to be used. In addition, there are also various third party Jinja extensions that you can find helpful in certain situations (e.g. Jinja statements that emulate Django template tags). Table 1 contains a list of extensions including their technical name that's used to enable them.

Table 1 - Jinja extensions with description and technical name
Extension functionalityDescriptionTechnical name
{% break %} and {% continue %} statementsOffers the ability to break and continue in template loops, just like the standard break and continue Python keywords.jinja2.ext.loopcontrols
{% do %} statementOffers the ability to evaluate an expression without producing output.jinja2.ext.do
{% with %} statementOffers the ability to define variables and limit their scope.jinja2.ext.with_
{% autoescape %} statementOffers the ability to enable/disable the escape of HTML characters from a template section.jinja2.ext.autoescape
{% csrf_token %}, {% trans %}, {% blocktrans %}, {% static %} and {% url %} statementsOffers the ability to use the equivalent functionality provided by Django tags with the same name.*jdj_tags.extensions.DjangoCompat (For all tags). See extension documenetation for more granular statement import names.
* All extensions with the exception of jdj_tags.extensions.DjangoCompat are part of Jinja itself, so they require no additional installation. To install jdj_tags.extensions.DjangoCompat use pip install jinja2-django-tags.

As you can see in table 1, the functionality provided by each extension varies and if you do an Internet search for 'Jinja2 extensions', you are sure to find a few more options that can save you time and work in various fronts.

Enable Jinja extensions

Jinja extensions are set up as part of Jinja's environment configuration, which in Django is configured in the OPTIONS variable of settings.py, as described in the recipe Use and customize Jinja templates in Django. Listing 1 illustrates a sample Django configuration that enables a series of Jinja extensions.

Listing 1 - Jinja extension configuration in Django

TEMPLATES = [
    { 
        'BACKEND':'django.template.backends.jinja2.Jinja2',
        'DIRS': ['%s/templates/'% (PROJECT_DIR),],
        'APP_DIRS': True,
        'OPTIONS': { 
            'extensions': [
                'jinja2.ext.loopcontrols',
                'jdj_tags.extensions.DjangoCompat',
                'coffeehouse.jinja.extensions.DjangoNow',
                ],
	}	
   }
]

As you can see in listing 1, we use the Jinja extension's name -- as described in table 1 -- and add it to a list that's assigned to the extensions key of the OPTIONS variable, which itself is part of Jinja's TEMPLATES Django configuration in settings.py. Note that the third extension coffeehouse.jinja.extensions.DjangoNow in listing 1 is a custom Jinja extension that I'll create in the final section of this recipe.

This is all that's necessary to enable a Jinja extension across all Jinja templates. Now that you know how to enable Jinja extensions, the next section explores how to create custom Jinja extensions.

Create Jinja extensions

Jinja has its own extension API which is thoroughly documented and tackles all the possible cases you may need an extension for. I won't attempt to use all of the API's functionality, because it would be nearly impossible to do so in a single example, instead I'll focus on creating a practical extension and in the process illustrate the layout and deployment process for a custom Jinja extension.

In Django templates when you want to output the current date or time, there's a tag named {% now %} for just this purpose, Jinja has no such statement, so I'll create a Jinja extension to mimic the same behavior as the Django template {% now %} tag. The Jinja {% now %} statement will function just like the Django template version and accept a format string, as well as the possibility to use the as keyword to define a variable with the value.

Listing 2 illustrates the source code for the custom Jinja extension that produces a Jinja {% now %} statement.

Listing 2 - Jinja custom extension for Jinja {% now %} statement.

from jinja2 import lexer, nodes
from jinja2.ext import Extension
from django.utils import timezone
from django.template.defaultfilters import date
from django.conf import settings
from datetime import datetime


class DjangoNow(Extension):
    tags = set(['now'])

    def _now(self, date_format):
        tzinfo = timezone.get_current_timezone() if settings.USE_TZ else None
        formatted = date(datetime.now(tz=tzinfo),date_format)
        return formatted


    def parse(self, parser):
        lineno = next(parser.stream).lineno
        token = parser.stream.expect(lexer.TOKEN_STRING)
        date_format = nodes.Const(token.value)
        call = self.call_method('_now', [date_format], lineno=lineno)
        token = parser.stream.current
        if token.test('name:as'):
            next(parser.stream)
            as_var = parser.stream.expect(lexer.TOKEN_NAME)
            as_var = nodes.Name(as_var.value, 'store', lineno=as_var.lineno)
            return nodes.Assign(as_var, call, lineno=lineno)
        else:
            return nodes.Output([call], lineno=lineno)

After the various import statements in listing 2, you can see we create the DjangoNow class that inherits its behavior from the jinja2.ext.Extension class, the last of which is part of Jinja and used for all custom extensions. Next, you can see we define the tags field with the set(['now']) value which is necessary to set up the statement/tag name. If you wanted the custom statement/tag to be called {% mytimer %} then you would declare tags = set(['mytimer']).

Next in listing 2 you can see the _now and parse methods. The _now method performs the actual current date or time calculation and checks the Django project's timezone configuration in settings.py -- a process that's just like Django's {% now %} tag. The parse method represents the entry point that executes the custom {% now %} statement/tag, where it uses the Jinja extension API to analyze the input and depending on the {% now %} declaration (e.g.{% now "F jS o" %}, {% now "F jS o" as today %}) executes the _now method and returns a result.

Once you create the custom Jinja extension, you need to declare it as part of the extensions variable on Jinja's environment configuration, as illustrated in listing 1. Note that based on the statement to import the custom Jinja extension in listing 1 -- coffeehouse.jinja.extensions.DjangoNow -- it's assumed the DjangoNow class in listing 2 is placed in a file/module named extensions under the coffeehouse.jinja directory path.

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

Listing 3 - Directory structure and location of custom Jinja extension
 
+---+-<PROJECT_DIR_coffeehouse>
    |
    +-__init__.py
    +-settings.py
    +-urls.py
    +-wsgi.py
    |
    +-jinja-+
            +-__init__.py
            +-extensions.py