Django template configuration
By default, Django templates are
enabled on all Django projects due to the TEMPLATES
variable in settings.py
. Listing 3-1 illustrates the
default TEMPLATES
value in Django projects.
Listing 3-1. Default Django template configuration in settings.py
TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ]
The BACKEND
variable
indicates the project uses Django templates. The DIRS
and APP_DIRS
variables tell Django where to locate
Django templates and are explained in the next section. The
context_processors
field inside OPTIONS
tells Django which context processors to enable for a Django
project. In short, a context processor offers the ability to share
data across all Django templates, without the need to define it in
a piecemeal fashion in Django views.
Later sections in this chapter describe what data is provided by default Django context processors and how to write your own context processors to share custom data on all Django templates.
Template search paths
Django determines where to look
for templates based on the values in the DIRS
and
APP_DIRS
variables. As you can see in listing 3-1,
Django defaults to an empty DIRS
value and sets the
APP_DIRS
variable to True.
The APP_DIRS
variable set to True
tells Django to look for
templates in Django app sub-folders named templates
--
if you've never heard of the Django app concept, look over chapter
1 which describes the concept of Django apps.
The APP_DIRS
behavior is helpful to contain an app's templates to an app's
structure, but be aware the template search path is not aware of an
app's namespace. For example, if you have two apps that both rely
on a template named index.html
and both app's have a
method in views.py
that returns control to the
index.html
template (e.g.render(request,'index.html')
), both app's will use the
index.html
from the top-most declared app in
INSTALLED_APPS
, so only one app will use the expected
index.html
.
The first set of folders illustrated in listing 3-2 shows two Django apps with this type of potential template layout conflict.
Listing 3-2. Django apps with templates dirs with potential conflict and namespace qualification
# Templates directly under templates folder can cause loading conflicts +---+-<PROJECT_DIR_project_name_conflict> | +-asgi.py +-__init__.py +-settings.py +-urls.py +-wsgi.py | +-about(app)-+ | +-__init__.py | +-models.py | +-tests.py | +-views.py | +-templates-+ | | | +-index.html +-stores(app)-+ +-__init__.py +-models.py +-tests.py +-views.py +-templates-+ | +-index.html # Templates classified with additional namespace avoid loading conflicts +---+-<PROJECT_DIR_project_name_namespace> | +-asgi.py +-__init__.py +-settings.py +-urls.py +-wsgi.py | +-about(app)-+ | +-__init__.py | +-models.py | +-tests.py | +-views.py | +-templates-+ | | | +-about-+ | | | +-index.html +-stores(app)-+ +-__init__.py +-models.py +-tests.py +-views.py +-templates-+ | +-stores-+ | +-index.html
To fix this potential template
search conflict, the recommended practice is to add an additional
sub-folder to act as a namespace inside each templates
directory, as illustrated in the second set of folders in listing
3-2.
In this manner, you can re-direct
control to a template using this additional namespace sub-folder to
avoid any ambiguity. So to send control to the
about/index.html
template you would declare
render(request,'about/index.html')
and to send control
to the stores/index.html
you would declare
render(request,'stores/index.html')
.
If you wish to disallow this
behavior of allowing templates to be loaded from these internal app
sub-folders, you can do so by setting APP_DIRS
to
FALSE
.
A more common approach to define
Django templates is to have a single folder or various folders that
live outside app structures to hold Django templates. In order for
Django to find such templates you use the DIRS
variable as illustrated in listing 3-3.
Listing 3-3. DIRS definition with relative path in settings.py
from pathlib import Path BASE_DIR = Path(__file__).resolve().parent.parent PROJECT_DIR = Path(__file__).resolve().parent TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [ PROJECT_DIR / 'templates', PROJECT_DIR / 'dev_templates'], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ]
As you can see in listing 3-3,
you can declare various directories inside the DIRS
variable. Django looks for templates inside DIRS
values and then in templates
folder in apps -- if
APP_DIRS
is True
-- until it either finds
a matching template or throws a TemplateDoesNotExist
error.
Also note the DIRS
values in listing 3-3 relies on a path determined dynamically by
the PROJECT_DIR
variable. This approach is helpful
when you deploy a Django project across different machines, because
the path is relative to the top-level Django project directory
(i.e. where the settings.py
and main
urls.py
file are) and adjusts dynamically irrespective
of where a Django project is installed (e.g.
/var/www/
, /opt/website/
,
C://website/
).
Invalid template variables
By default, Django templates do not throw an error when they contain invalid variables. This is due to design choices associated with the Django admin which also uses Django templates.
While this is not a major issue
in most cases, it can be frustrating for debugging tasks as Django
doesn't inform you of misspelled or undefined variables. For
example, you could type {{datee}}
instead of
{{date}}
and Django ignores this by outputting an
empty string ''
, you could also forget to pass a
variable value to a template in the view method and Django also
silently outputs an empty string ''
even though you
may have it defined in the template.
To enable Django to inform you
when it encounters an invalid variable in Django templates, you can
use the string_if_invalid
option. The first
configuration option for string_if_invalid
shown in
listing 3-4 outputs a visible string instead of an empty string
''
.
Listing 3-4 Output warning message for invalid template variables with string_if_invalid
from pathlib import Path BASE_DIR = Path(__file__).resolve().parent.parent PROJECT_DIR = Path(__file__).resolve().parent TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [ PROJECT_DIR / 'templates', PROJECT_DIR / 'dev_templates'] 'APP_DIRS': True, 'OPTIONS': { 'string_if_invalid': "**** WARNING INVALID VARIABLE %s ****", 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ]
As you can see in listing 3-4,
string_if_invalid
is assigned the string "****
WARNING INVALID VARIABLE %s ****"
. When Django encounters an
invalid variable, it replaces the occurrence with this string,
where the %s
variable gets substituted with the
invalid variable name, allowing you to easily locate where and what
variables are invalid.
Another configuration option for
the string_if_invalid
option is to perform more
complex logic when an invalid variable is encountered. For example,
listing 3-5 illustrates how you can raise an error so the template
fails to render in case an invalid variable is found.
Listing 3-5. Error generation for invalid template variables with string_if_invalid
from pathlib import Path BASE_DIR = Path(__file__).resolve().parent.parent PROJECT_DIR = Path(__file__).resolve().parent class InvalidTemplateVariable(str): def __mod__(self,other): from django.template.base import TemplateSyntaxError raise TemplateSyntaxError("Invalid variable : '%s'" % other) TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [ PROJECT_DIR / 'templates', PROJECT_DIR / 'dev_templates'] 'APP_DIRS': True, 'OPTIONS': { 'string_if_invalid': InvalidTemplateVariable("%s"), 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ]
In listing 3-5
string_if_invalid
is assigned the
InvalidTemplateVariable
class that uses the
%s
input variable, which represents the invalid
variable name -- just like the previous example in listing 3-4.
The
InvalidTemplateVariable
class is interesting because
it inherits its behavior from the str(string)
class
and uses a Python __mod__
(modulo) magic method
implementation. While the __mod__
(modulo) magic
method is proper of number operations, in this case, it's useful
because the passed in string uses the %
(modulo)
symbol, which makes the __mod__
method run. Inside the
__mod__
method we just raise the
TemplateSyntaxError
error with the invalid variable
name to halt the execution of the template.
Caution The Django admin might get mangled or broken with a customstring_if_invalid
value.The Django admin templates in particular rely heavily on the defaultstring_if_invalid
outputting empty strings''
, due to the level of complexity in certain displays. In fact, thisstring_if_invalid
default behavior is often considered a 'feature', as much as it's considered a 'bug' or 'annoyance'.Therefore if you use one of the approaches in listing 3-4 or listing 3-5 to overridestring_if_invalid
, be aware you will most likely mangle or brake Django admin pages. If you rely on the Django admin, you should only use these techniques to debug a project's templates.
Debug output
When you run a Django project
with the top-level DEBUG=True
setting and an error
occurs, Django templates output a very detailed page to make the
debugging process easier -- see the chapter Django application management for more details on
the DEBUG
variable, specifically the section Django
settings.py
for the real world.
By default, Django templates
reuse the top-level DEBUG
variable value to configure
template debug activity. Behind the scenes, this configuration is
set through the debug field inside OPTIONS
of the
TEMPLATES
variable. Figure 3-1 illustrates what an
error page looks when DEBUG=True
.
Figure 3-1. Django error page when DEBUG=True automatically sets template OPTION to 'debug':True
As you can see in figure 3-1,
Django prints the location of the template, as well as a snippet of
the template itself to make it easier to locate an error. This
template information is generated due to the
'debug':True
option, which is set based on the
top-level DEBUG
variable. However, you can explicitly
set the debug option to False
as illustrated in
listing 3-6, in which case the error page would result without any
template details and just the traceback information, as illustrated
in Figure 3-2.
Listing 3-6.Option with debug equals False omits template details
from pathlib import Path BASE_DIR = Path(__file__).resolve().parent.parent PROJECT_DIR = Path(__file__).resolve().parent TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [ PROJECT_DIR / 'templates', PROJECT_DIR / 'dev_templates'] 'APP_DIRS': True, 'OPTIONS': { 'debug':False, 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ]
Figure 3-2. Django error page when DEBUG=True and explicit OPTION 'debug':False
Auto-escape
By default, Django templates use the security practice to auto-escape certain characters -- described in table 3-1 -- contained in dynamically generated constructs (e.g. variables, tags, filters). Auto-escaping converts characters that can potentially mangle a user interface or produce dangerous outcomes into safe representations, a process that was described in the first section of this chapter.
However, you can globally disable
auto-escaping on all Django templates -- and knowingly render <
as <, > as >, etc... -- with the autoescape
field in OPTIONS
as shown in listing 3-7.
Listing 3-7 Option with autoescape equals False omits autoescaping on all Django templates
from pathlib import Path BASE_DIR = Path(__file__).resolve().parent.parent PROJECT_DIR = Path(__file__).resolve().parent TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [ PROJECT_DIR / 'templates', PROJECT_DIR / 'dev_templates'] 'APP_DIRS': True, 'OPTIONS': { 'autoescape':False, 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ]
It's worth mentioning that an
alternative to disabling auto-escaping on every Django template --
as it's done in listing 3-7 -- is to selectively disable
auto-escaping. You can either use the {% autoescape off
%}
tag to disable auto-escaping on a section of a Django
template or the safe
filter to disable auto-escaping
on a single Django template variable.
If you do decide to disable
auto-escaping on all Django templates as illustrated in listing 3-7
-- which I frankly wouldn't recommend if you plan to use just HTML,
because of the potential security risk -- you can also granularly
enable auto-escaping again if required. You can either use the
{% autoescape on %}
tag to enable auto-escaping on a
section of a Django template or the escape
filter to
escape a single Django template variable.
File charset
Files used in Python projects
often declare an encoding value at the top (e.g. # -*-
coding: utf-8 -*-
) based on the Python PEP-263
specification[2], which ensures the characters in the
file are interpreted correctly. In Django templates, you don't
define the underlying file's encoding in this manner, but instead
do it inside a project's settings.py
file.
There are two ways to declare the
encoding character for Django templates: explicitly as part of the
file_charset
field in OPTIONS
inside the
TEMPLATES
variable or via the top level
FILE_CHARSET
variable in settings.py
. The
explicit declaration in file_charset
within
OPTIONS
takes precedence over the
FILE_CHARSET
assignment, but the value of
file_charset
defaults to FILE_CHARSET
which in itself defaults to utf-8 (Unicode) encoding.
So by default, Django template
encoding is assigned to utf-8 or Unicode which is one of the most
widely used encodings in software. Nevertheless, in the event you
decide to incorporate data into Django templates that isn't utf-8
compatible (e.g. Spanish vowels with accents like á or
é encoded as ISO-8859-1 or Kanji characters like 漢 or
字 encoded as JIS) you must define the
FILE_CHARSET
value in a project's
settings.py
file -- or directly in the
file_charset
field in OPTIONS
inside the
TEMPLATES
-- so Django template data is interpreted
correctly.
Django templates can be assigned any encoding value from Python's standard encoding values[3].
Automatic access to custom template tag/filter modules
Django templates have access to a
series of built-in tags & filters that don't require any setup
steps. However, if you plan to use a third-party template
tag/filter module or write your own template tag/filter module,
then you need to setup access on each Django template with the
{% load %}
tag (e.g. {% load
really_useful_tags_and_filters %}
), a process that can get
tiresome if you need to access a particular tag/filter on dozens or
hundreds of templates.
To automatically gain access to
third-party template tags/filters or your own template tags/filters
as if they were built-in tags/filters (i.e. without requiring the
{% load %}
tag), you can use the builtins
field in OPTIONS
as illustrated in listing 3-8.
Listing 3-8 Option with builtins to gain automatic access to tags/filters on all templates
from pathlib import Path BASE_DIR = Path(__file__).resolve().parent.parent PROJECT_DIR = Path(__file__).resolve().parent TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [ PROJECT_DIR / 'templates', PROJECT_DIR / 'dev_templates'], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], 'builtins': [ 'coffeehouse.builtins', 'thirdpartyapp.customtags.really_useful_tags_and_filters', ], }, }, ]
As you can see in listing 3-8,
the builtins
field accepts a list of modules that
includes tags/filters for built-in treatment. In this case,
coffeehouse.builtins
represents a
builtins.py
file -- that includes the custom
tags/filters -- under a project named coffeehouse
. And
the
thirdpartyapp.customtags.really_useful_tags_and_filters
is a third-party package with tags/filters that we also want to
access in Django templates without the need to use the {%
load %}
tag.
Another default behavior of
third-party template tag/filter modules and custom template
tag/filter modules is they require to use their original label/name
for reference, while the latter also requires to be placed inside a
folder named templatetags
in a registered Django app.
These two default behaviors can be overridden with the
libraries
field in OPTIONS
as illustrated
in listing 3-9.
Listing 3-9 Option with libraries to register tags/filters with alternative label/name and under any project directory.
from pathlib import Path BASE_DIR = Path(__file__).resolve().parent.parent PROJECT_DIR = Path(__file__).resolve().parent TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [ PROJECT_DIR / 'templates', PROJECT_DIR / 'dev_templates'], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], 'libraries': { 'coffeehouse_tags': 'coffeehouse.tags_filters.common', }, }, }, ]
The libraries statement in
listing 3-9 'coffeehouse_tags':
'coffeehouse.tags_filters.common'
tells Django to load the
common.py
file -- that includes the custom
tags/filters -- from the tags_filters
folder in the
coffeehouse
project and make it accessible to
templates through the coffeehouse_tags
reference (e.g.
{% load coffeehouse_tags %}
). With the approach in
listing 3-9, you can place custom tag/filter modules anywhere in a
Django project, as well as assign custom tag/filter modules -- or
third-party tag/filter modules -- an alternative reference value
instead of their original label/name.
Template loaders
Earlier in the section 'Template search paths', I described how Django searches for templates using
the DIRS
and APP_DIRS
variables which are
part of Django's template configuration. However, I intentionally
omitted a deeper aspect associated with this template search
process: each search mechanism is backed by a template loader.
A template loader is a Python class that implements the actual logic required to search & load templates. Table 3-2 illustrates the built-in template loaders available in Django.
Table 3-2. Built-in Django template loaders
Template loader class | Description |
---|---|
django.template.loaders.app_directories.Loader |
Searches and loads templates from sub-directories named templates in all apps declared in INSTALLED_APPS. Enabled by default when APP_DIRS is True. |
django.template.loaders.cached.Loader |
Searches for templates from an in-memory cache, after loading templates from a file-system or app directory loader. |
django.template.loaders.filesystem.Loader |
Searches and loads templates in directories declared in the DIRS variable. Enabled by default when DIRS is not empty. |
django.template.loaders.locmem.Loader |
Searches for templates from an in-memory cache, after loading templates from a Python dictionary. |
The Django template loaders that are automatically enabled are dependent on configuration parameters. By default, the django.template.loaders.filesystem.Loader
template loader is always available, if 'APP_DIRS': True
is set then the django.template.loaders.app_directories.Loader
template loader is also enabled, and if DEBUG == False
then the django.template.loaders.cached.Loader
template loader is also enabled.
Nevertheless, it's possible to override this default behavior by explicitly specifying a list of template loaders using the loaders
field in OPTIONS
inside TEMPLATES
.