Jinja extensions (like Django template library tags)

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 4-2. Jinja extensions with description and technical name

Extension functionality Description Technical name
{% break %} and {% continue %} statements Offers the ability to break and continue in template loops, just like the standard break and continue Python keywords. jinja2.ext.loopcontrols
{% do %} statement Offers the ability to evaluate an expression without producing output. jinja2.ext.do
{% trans %} statement Offers the ability to apply i18n to templates (i.e. block gettext translations). jinja2.ext.i18n
{% debug %} statement Offers the ability to use view the current context as well as the available filters and tests applied to a template. jinja2.ext.debug

* All extensions are included as part of Jinja, they require no additional installation, they just need to be enabled.

As you can see in table 4-2, 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.

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 enable it with the extensions key of the OPTIONS variable in settings.py.

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 previous section on configuring Jinja templates in Django. Listing 4-27 illustrates a sample Django configuration that enables a series of Jinja extensions.

Listing 4-27. Jinja extension configuration in Django

TEMPLATES = [
    { 
        'BACKEND':'django.template.backends.jinja2.Jinja2',
	'DIRS': [ PROJECT_DIR / 'jinjatemplates' ],	
        'APP_DIRS': True,
        'OPTIONS': { 
            'extensions': [
                'jinja2.ext.loopcontrols',
		'jinja2.ext.debug',
                'coffeehouse.jinja.extensions.DjangoNow',
                ],
        }       
   }
]

As you can see in listing 4-27, we use the Jinja extension's name -- as described in table 4-2 -- 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 4-27 is a custom Jinja extension that I'll create in the next and final section of this chapter.

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[7] 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 4-28 illustrates the source code for the custom Jinja extension that produces a Jinja {% now %} statement.

Listing 4-28. 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 4-28, 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 4-28 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 4-27.

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

Listing 4-29 Directory structure and location of custom Jinja extension

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

Note that based on the statement to import the custom Jinja extension in listing 4-27 -- coffeehouse.jinja.extensions.DjangoNow -- it's assumed the DjangoNow class in listing 4-28 is placed in a file/module named extensions.py under the coffeehouse.jinja directory path.

  1. https://jinja.palletsprojects.com/en/3.0.x/extensions/