Шаблоны
Как веб-фреймворк, Django нуждается в удобном способе динамической генерации HTML. Наиболее распространённый подход использует шаблоны. Шаблон содержит статическую часть желаемого HTML-вывода, а также специальный синтаксис, описывающий, как будет вставлено динамическое содержимое. Для практического примера создания HTML-страниц с помощью шаблонов см. Учебник 3.
Проект Django можно настроить с одним или несколькими движками шаблонов (или даже с нулём, если вы не используете шаблоны). Django поставляется со встроенными бэкендами для собственной системы шаблонов, креативно названной языком шаблонов Django (DTL), и для популярной альтернативы Jinja2. Бэкэнды для других языков шаблонов могут быть доступны от сторонних разработчиков.
Django определяет стандартный API для загрузки и рендеринга шаблонов независимо от бэкэнда. Загрузка включает в себя поиск шаблона по заданному идентификатору и его предварительную обработку, обычно компиляцию в представление в памяти. Рендеринг означает интерполяцию шаблона с данными контекста и возвращение полученной строки.
Язык шаблонов Django — собственная система шаблонов Django. До версии Django 1.8 это был единственный встроенный вариант. Это хорошая библиотека шаблонов, хотя и довольно предвзятая и имеет несколько особенностей. Если у вас нет веских причин выбирать другой бэкенд, вы должны использовать DTL, особенно если вы пишете подключаемый модуль и планируете распространять шаблоны. Приложения contrib Django, которые включают шаблоны, такие как django.contrib.admin, используют DTL.
По историческим причинам как общая поддержка движков шаблонов, так и реализация языка шаблонов Django находятся в пространстве имён django.template.
Предупреждение
Система шаблонов не защищена от ненадёжных авторов шаблонов. Например, сайт не должен разрешать своим пользователям предоставлять собственные шаблоны, так как авторы шаблонов могут выполнять такие действия, как XSS-атаки и доступ к свойствам переменных шаблонов, которые могут содержать конфиденциальную информацию.
Поддержка движков шаблонов
Настройка
Движки шаблонов настраиваются с помощью настройки TEMPLATES. Это список конфигураций, по одной для каждого движка. Значение по умолчанию — пустое. Значение, сгенерированное командой startproject, более полезно:
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
# ... some options here ...
},
},
]
BACKEND — это путь в Python к классу движка шаблонов, реализующему API бэкэнда Django для шаблонов. Встроенные бэкэнды — django.template.backends.django.DjangoTemplates и django.template.backends.jinja2.Jinja2.
Поскольку большинство движков загружают шаблоны из файлов, верхнеуровневая конфигурация каждого движка содержит две общие настройки:
-
DIRSопределяет список каталогов, где движок должен искать файлы шаблонов в порядке поиска. -
APP_DIRSуказывает, должен ли движок искать шаблоны внутри установленных приложений. Каждый бэкенд определяет стандартное имя для подкаталога внутри приложений, где должны храниться его шаблоны.
Хотя это нечасто используется, можно настроить несколько экземпляров одного бэкэнда с различными параметрами. В этом случае вы должны определить уникальное значение NAME для каждого движка.
OPTIONS содержит настройки, специфичные для бэкэнда.
Использование
Модуль django.template.loader определяет две функции для загрузки шаблонов.
-
get_template(template_name, using=None)[source] -
Эта функция загружает шаблон с заданным именем и возвращает объект
Template.Точный тип возвращаемого значения зависит от бэкэнда, который загрузил шаблон. Каждый бэкенд имеет свой собственный класс
Template.get_template()перебирает каждый движок шаблонов в порядке, пока один из них не преуспеет. Если шаблон не найден, он вызывает исключениеTemplateDoesNotExist. Если шаблон найден, но содержит недопустимый синтаксис, он вызывает исключениеTemplateSyntaxError.Как ищутся и загружаются шаблоны, зависит от бэкэнда каждого движка и его конфигурации.
Если вы хотите ограничить поиск определённым движком шаблонов, передайте имя бэкэнда
NAMEв аргументusing.
-
select_template(template_name_list, using=None)[source] -
select_template()аналогичнаget_template(), за исключением того, что она принимает список имён шаблонов. Она перебирает каждое имя в порядке и возвращает первый найденный шаблон.
Если загрузка шаблона завершается неудачно, могут быть вызваны следующие два исключения, определённые в django.template:
-
exception TemplateDoesNotExist(msg, tried=None, backend=None, chain=None)[source] -
Это исключение возникает, когда шаблон не найден. Оно принимает следующие необязательные аргументы для заполнения постморта шаблона на странице отладки:
-
backend - Экземпляр бэкэнда шаблона, из которого произошло исключение.
-
tried - Список источников, которые были проверены при поиске шаблона. Он отформатирован как список кортежей, содержащих
(origin, status), гдеorigin— объект типа «происхождение», аstatus— строка с причиной, по которой шаблон не был найден. -
chain - Список промежуточных исключений
TemplateDoesNotExist, возникших при попытке загрузить шаблон. Это используется функциями, такими какget_template(), которые пытаются загрузить заданный шаблон из нескольких движков.
-
-
exception TemplateSyntaxError(msg)[source] -
Это исключение возникает, когда шаблон был найден, но содержит ошибки.
Объекты Template , возвращаемые get_template() и select_template() , должны предоставлять метод render() со следующей подписью:
-
Template.render(context=None, request=None) -
Рендерит данный шаблон с заданным контекстом.
Если
contextпредоставлен, это должен бытьdict. Если он не предоставлен, движок отрендерит шаблон с пустым контекстом.Если
requestпредоставлен, это должен бытьHttpRequest. Тогда движок должен сделать его, а также токен CSRF, доступными в шаблоне. Как это достигается, зависит от каждого бэкэнда.
Вот пример алгоритма поиска. Для этого примера настройка TEMPLATES:
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
'/home/html/example.com',
'/home/html/default',
],
},
{
'BACKEND': 'django.template.backends.jinja2.Jinja2',
'DIRS': [
'/home/html/jinja2',
],
},
]
Если вы вызовете get_template('story_detail.html'), вот какие файлы Django будет искать в порядке:
-
/home/html/example.com/story_detail.html('django'движок) -
/home/html/default/story_detail.html('django'движок) -
/home/html/jinja2/story_detail.html('jinja2'движок)
Если вы вызовете select_template(['story_253_detail.html', 'story_detail.html']), вот что будет искать Django:
-
/home/html/example.com/story_253_detail.html('django'движок) -
/home/html/default/story_253_detail.html('django'движок) -
/home/html/jinja2/story_253_detail.html('jinja2'движок) -
/home/html/example.com/story_detail.html('django'движок) -
/home/html/default/story_detail.html('django'движок) -
/home/html/jinja2/story_detail.html('jinja2'движок)
Когда Django найдёт существующий шаблон, он перестаёт искать.
Подсказка
Вы можете использовать select_template() для гибкой загрузки шаблонов. Например, если вы написали новостную статью и хотите, чтобы некоторые статьи имели пользовательские шаблоны, используйте что-то вроде select_template(['story_%s_detail.html' % story.id,
'story_detail.html']). Это позволит вам использовать пользовательский шаблон для отдельной статьи со стандартным шаблоном по умолчанию для статей, не имеющих пользовательских шаблонов.
Возможна — и предпочтительна — организация шаблонов в подкаталогах внутри каждого каталога, содержащего шаблоны. Конвенцией является создание подкаталога для каждого приложения Django, с подкаталогами внутри них по мере необходимости.
Сделайте это для вашего удобства. Хранение всех шаблонов в корневом уровне одного каталога становится запутанным.
END_OF_DOCUMENT_MARKERДля загрузки шаблона, находящегося в подкаталоге, просто используйте слеш, как показано ниже:
get_template('news/story_detail.html')
Используя тот же параметр TEMPLATES, что и выше, будут попытки загрузить следующие шаблоны:
-
/home/html/example.com/news/story_detail.html('django'движок) -
/home/html/default/news/story_detail.html('django'движок) -
/home/html/jinja2/news/story_detail.html('jinja2'движок)
Кроме того, чтобы сократить повторяющиеся действия по загрузке и рендерингу шаблонов, Django предоставляет функцию-ярлык, которая автоматизирует этот процесс.
-
render_to_string(template_name, context=None, request=None, using=None)[source] -
render_to_string()загружает шаблон, какget_template()и сразу вызывает его методrender(). Он принимает следующие аргументы.-
template_name - Имя шаблона для загрузки и рендеринга. Если это список имён шаблонов, Django использует
select_template()вместоget_template()для поиска шаблона. -
context dictдля использования в качестве контекста шаблона для рендеринга.-
request - Необязательный
HttpRequest, который будет доступен во время процесса рендеринга шаблона. -
using - Необязательный движок шаблонов
NAME. Поиск шаблона будет ограничен этим движком.
Пример использования:
from django.template.loader import render_to_string rendered = render_to_string('my_template.html', {'foo': 'bar'}) -
См. также ярлык render(), который вызывает render_to_string() и передаёт результат в HttpResponse подходящий для возврата из представления.
Наконец, вы можете напрямую использовать настроенные движки:
-
engines -
Движки шаблонов доступны в
django.template.engines:from django.template import engines django_engine = engines['django'] template = django_engine.from_string("Hello {{ name }}!")Ключ поиска —
'django'в этом примере — этоNAMEдвижка.
Встроенные движки
-
class DjangoTemplates[source]
Установите BACKEND на 'django.template.backends.django.DjangoTemplates' для настройки движка Django шаблонов.
Когда APP_DIRS равно True, движки DjangoTemplates ищут шаблоны в подкаталоге templates установленных приложений. Это общее имя сохранено для обратной совместимости.
Движки DjangoTemplates принимают следующие OPTIONS:
-
'autoescape': логическое значение, определяющее, включена ли автоматическая экранировка HTML.По умолчанию
True.Предупреждение
Устанавливайте его только в
Falseслучае рендеринга шаблонов, не являющихся HTML!Добавлено в Django 1.10:Параметр
autoescapeбыл добавлен. -
'context_processors': список имён Python-функций, используемых для заполнения контекста при рендеринге шаблона с запросом. Эти функции принимают объект запроса в качестве аргумента и возвращаютdictэлементов, которые будут объединены в контекст.По умолчанию пустой список.
См.
RequestContextдля получения дополнительной информации. -
'debug': логическое значение, включающее/отключающее режим отладки шаблона. Если онTrue, страница ошибки отобразит подробный отчёт об исключении, возникшем во время рендеринга шаблона. Этот отчёт содержит соответствующий фрагмент шаблона с выделенной строкой.По умолчанию значение настройки
DEBUG. -
'loaders': список имён Python-классов загрузчиков шаблонов. КаждыйLoaderкласс знает, как импортировать шаблоны из определённого источника. Вместо строки также можно использовать кортеж. Первый элемент в кортеже должен быть именем классаLoader, а последующие элементы передаютсяLoaderво время инициализации.Значение по умолчанию зависит от значений
DIRSиAPP_DIRS.См. Типы загрузчиков для получения подробностей.
-
'string_if_invalid': строковое значение, которое система шаблонов должна использовать для недопустимых (например, неверно написанных) переменных.По умолчанию пустая строка.
См. Как обрабатываются недопустимые переменные шаблона для получения подробностей.
-
'file_charset': кодировка, используемая для чтения файлов шаблонов на диске.По умолчанию значение настройки
FILE_CHARSET. -
'libraries': словарь меток и имён Python-путей модулей тегов шаблонов для регистрации в движке шаблонов. Это можно использовать для добавления новых библиотек или предоставления альтернативных меток для существующих. Например:OPTIONS={ 'libraries': { 'myapp_tags': 'path.to.myapp.tags', 'admin.urls': 'django.contrib.admin.templatetags.admin_urls', }, }Библиотеки могут загружаться путём передачи соответствующего ключа словаря тегу
{% load %}. -
'builtins': список имён Python-путей модулей тегов шаблонов для добавления в встроенные. Например:OPTIONS={ 'builtins': ['myapp.builtins'], }Теги и фильтры из встроенных библиотек могут использоваться без предварительного вызова тега
{% load %}.
-
class Jinja2[source]
Требуется установка Jinja2:
$ pip install Jinja2
Установите BACKEND на 'django.template.backends.jinja2.Jinja2' для настройки движка Jinja2.
Когда APP_DIRS равно True, движки Jinja2 ищут шаблоны в подкаталоге jinja2 установленных приложений.
Самая важная запись в OPTIONS это 'environment'. Это Python-путь к вызываемому элементу, возвращающему среду Jinja2. По умолчанию 'jinja2.Environment'. Django вызывает этот вызываемый элемент и передаёт другие параметры как ключевые аргументы. Кроме того, Django добавляет значения по умолчанию, отличные от значений Jinja2, для нескольких параметров:
-
'autoescape':True -
'loader': загрузчик, настроенный дляDIRSиAPP_DIRS -
'auto_reload':settings.DEBUG -
'undefined':DebugUndefined if settings.DEBUG else Undefined
Движки Jinja2 также принимают следующие OPTIONS:
-
'context_processors': список пунктирных путей Python к вызываемым функциям, которые используются для заполнения контекста при рендеринге шаблона с запросом. Эти вызываемые функции принимают объект запроса в качестве аргумента и возвращаютdictэлементов для слияния в контекст.По умолчанию устанавливается пустой список.
Использование обработчиков контекста с шаблонами Jinja2 не рекомендуется.
Обработчики контекста полезны с шаблонами Django, так как шаблоны Django не поддерживают вызов функций с аргументами. Поскольку Jinja2 не имеет этого ограничения, рекомендуется поместить функцию, которая используется как обработчик контекста, во глобальные переменные, доступные для шаблона, используя
jinja2.Environmentкак описано ниже. Затем вы можете вызвать эту функцию в шаблоне:{{ function(request) }}Некоторые обработчики контекста шаблонов Django возвращают фиксированное значение. Для шаблонов Jinja2 такой уровень косвенности не нужен, так как вы можете добавить константы непосредственно в
jinja2.Environment.Исходный случай использования добавления обработчиков контекста для Jinja2 включал:
- Выполнение дорогостоящих вычислений, зависящих от запроса.
- Необходимость результата во всех шаблонах.
- Использование результата несколько раз в каждом шаблоне.
Если не выполняются все эти условия, передача функции в шаблон проще и более соответствует принципам проектирования Jinja2.
Был добавлен параметр 'context_processors'.
Конфигурация по умолчанию намеренно минимальна. Если шаблон рендерится с запросом (например, при использовании render()), бэкенд Jinja2 добавляет глобальные переменные request, csrf_input, и csrf_token в контекст. Помимо этого, этот бэкенд не создаёт среду с Джанго-образными настройками. Он не знает о фильтрах и тегах Django. Для использования специфичных для Django API, необходимо настроить их в среде.
Например, вы можете создать myproject/jinja2.py с этим содержимым:
from __future__ import absolute_import # Python 2 only
from django.contrib.staticfiles.storage import staticfiles_storage
from django.urls import reverse
from jinja2 import Environment
def environment(**options):
env = Environment(**options)
env.globals.update({
'static': staticfiles_storage.url,
'url': reverse,
})
return env
и установить параметр 'environment' в 'myproject.jinja2.environment'.
Затем вы можете использовать следующие конструкции в шаблонах Jinja2:
<img src="{{ static('path/to/company-logo.png') }}" alt="Company Logo">
<a href="{{ url('admin:index') }}">Administration</a>
Понятия тегов и фильтров существуют как в языке шаблонов Django, так и в Jinja2, но используются по-разному. Поскольку Jinja2 поддерживает передачу аргументов вызываемым функциям в шаблонах, многие функции, которые требуют тега или фильтра в шаблонах Django, можно реализовать просто вызовом функции в шаблонах Jinja2, как показано в примере выше. Глобальное пространство имён Jinja2 избавляет от необходимости использования обработчиков контекста шаблонов. В языке шаблонов Django нет аналога Jinja2 тестов.
Кастомные бэкенды
Вот как реализовать кастомный бэкенд шаблонов для использования другой системы шаблонов. Бэкенд шаблонов — это класс, который наследует django.template.backends.base.BaseEngine. Он должен реализовывать get_template() и необязательно from_string(). Вот пример для вымышленной библиотеки шаблонов foobar:
from django.template import TemplateDoesNotExist, TemplateSyntaxError
from django.template.backends.base import BaseEngine
from django.template.backends.utils import csrf_input_lazy, csrf_token_lazy
import foobar
class FooBar(BaseEngine):
# Name of the subdirectory containing the templates for this engine
# inside an installed application.
app_dirname = 'foobar'
def __init__(self, params):
params = params.copy()
options = params.pop('OPTIONS').copy()
super(FooBar, self).__init__(params)
self.engine = foobar.Engine(**options)
def from_string(self, template_code):
try:
return Template(self.engine.from_string(template_code))
except foobar.TemplateCompilationFailed as exc:
raise TemplateSyntaxError(exc.args)
def get_template(self, template_name):
try:
return Template(self.engine.get_template(template_name))
except foobar.TemplateNotFound as exc:
raise TemplateDoesNotExist(exc.args, backend=self)
except foobar.TemplateCompilationFailed as exc:
raise TemplateSyntaxError(exc.args)
class Template(object):
def __init__(self, template):
self.template = template
def render(self, context=None, request=None):
if context is None:
context = {}
if request is not None:
context['request'] = request
context['csrf_input'] = csrf_input_lazy(request)
context['csrf_token'] = csrf_token_lazy(request)
return self.template.render(context)
См. DEP 182 для получения дополнительной информации.
Интеграция отладки для кастомных движков
Страница отладки Django имеет крючки для предоставления подробной информации при возникновении ошибки шаблона. Кастомные движки шаблонов могут использовать эти крючки для улучшения информации об отслеживании ошибок, отображаемой пользователям. Доступны следующие крючки:
Постмортальная информация о шаблоне
Постмортальная информация отображается при возникновении ошибки TemplateDoesNotExist. Она перечисляет движки шаблонов и загрузчики, которые использовались при поиске данного шаблона. Например, если настроены два движка Django, постмортальная информация будет выглядеть так:
Кастомные движки могут заполнить постмортальную информацию, передав backend и tried аргументы при возникновении ошибки TemplateDoesNotExist. Бэкенды, использующие постмортальную информацию, должны указать источник в объекте шаблона.
Контекстная информация о строке
Если во время анализа или рендеринга шаблона произошла ошибка, Django может отобразить строку, в которой произошла ошибка. Например:
Кастомные движки могут заполнить эту информацию, задав атрибут template_debug для исключений, возникающих во время анализа и рендеринга. Этот атрибут является dict со следующими значениями:
-
'name': имя шаблона, в котором произошла ошибка. -
'message': сообщение об ошибке. -
'source_lines': строки перед, после и включая строку, в которой произошла ошибка. Это контекст, поэтому он не должен содержать более 20 строк. -
'line': номер строки, в которой произошла ошибка. -
'before': содержимое строки ошибки до токена, вызвавшего ошибку. -
'during': токен, вызвавший ошибку. -
'after': содержимое строки ошибки после токена, вызвавшего ошибку. -
'total': количество строк вsource_lines. -
'top': номер строки, где начинаетсяsource_lines. -
'bottom': номер строки, где заканчиваетсяsource_lines.
Учитывая вышеприведённую ошибку шаблона, template_debug будет выглядеть так:
{
'name': '/path/to/template.html',
'message': "Invalid block tag: 'syntax'",
'source_lines': [
(1, 'some\n'),
(2, 'lines\n'),
(3, 'before\n'),
(4, 'Hello {% syntax error %} {{ world }}\n'),
(5, 'some\n'),
(6, 'lines\n'),
(7, 'after\n'),
(8, ''),
],
'line': 4,
'before': 'Hello ',
'during': '{% syntax error %}',
'after': ' {{ world }}\n',
'total': 9,
'bottom': 9,
'top': 1,
}
API источника и интеграция сторонних библиотек
Шаблоны Django имеют доступ к объекту Origin через атрибут template.origin. Это позволяет отображать отладочную информацию в постмортальной информации о шаблоне, а также в сторонних библиотеках, таких как Django Debug Toolbar.
Кастомные движки могут предоставлять собственную информацию о template.origin, создав объект, который указывает следующие атрибуты:
-
'name': полный путь к шаблону. -
'template_name': относительный путь к шаблону, переданный в методы загрузки шаблонов. -
'loader_name': необязательная строка, идентифицирующая функцию или класс, используемый для загрузки шаблона, напримерdjango.template.loaders.filesystem.Loader.
Язык шаблонов Django
Синтаксис
О данной секции
Это обзор синтаксиса языка шаблонов Django. Для подробной информации см. справочник по синтаксису языка.
Шаблон Django — это просто текстовый документ или строка Python, помеченная языком шаблонов Django. Некоторые конструкции распознаются и интерпретируются движком шаблонов. Основные из них — переменные и теги.
Шаблон рендерится с контекстом. Рендеринг заменяет переменные их значениями, которые ищутся в контексте, и выполняет теги. Всё остальное выводится как есть.
Синтаксис языка шаблонов Django включает четыре конструкции.
Переменные
Переменная выводит значение из контекста, который представляет собой объект, подобный словарю, сопоставляющий ключи со значениями.
Переменные заключены в {{ и }} так:
My first name is {{ first_name }}. My last name is {{ last_name }}.
С контекстом {'first_name': 'John', 'last_name': 'Doe'}, этот шаблон отобразит:
My first name is John. My last name is Doe.
Поиск по словарям, поиск по атрибутам и поиск по индексам списков реализуются с помощью обозначения точек:
{{ my_dict.key }}
{{ my_object.attribute }}
{{ my_list.0 }}
Если переменная разрешается в вызываемую функцию, система шаблонов вызовет её без аргументов и использует её результат вместо самой функции.
Теги
Это определение умышленно расплывчато. Например, тег может выводить содержимое, служить структурой управления, например, оператором «if» или циклом «for», извлекать содержимое из базы данных или даже предоставлять доступ к другим тегам шаблонов.
Теги заключены в {% и %} так:
{% csrf_token %}
Большинство тегов принимают аргументы:
{% cycle 'odd' 'even' %}
Некоторые теги требуют начального и конечного тегов:
{% if user.is_authenticated %}Hello, {{ user.username }}.{% endif %}
Доступен справочник по встроенным тегам, а также инструкции по написанию кастомных тегов.
Фильтры
Фильтры преобразуют значения переменных и аргументов тегов.
Они выглядят так:
{{ django|title }}
С контекстом {'django': 'the web framework for perfectionists with
deadlines'}, этот шаблон отобразит:
The Web Framework For Perfectionists With Deadlines
Некоторые фильтры принимают аргумент:
{{ my_date|date:"Y-m-d" }}
Доступен справочник по встроенным фильтрам, а также инструкции по написанию кастомных фильтров.
Комментарии
Комментарии выглядят так:
{# this won't be rendered #}
Тег {% comment %} обеспечивает многострочные комментарии.
Компоненты
О данной секции
Это обзор API языка шаблонов Django. Для получения подробностей см. справочник API.
Двигатель
django.template.Engine инкапсулирует экземпляр системы шаблонов Django. Основная причина непосредственного создания экземпляра Engine заключается в использовании языка шаблонов Django вне проекта Django.
django.template.backends.django.DjangoTemplates — это тонкий обертка, адаптирующий django.template.Engine к API бэкенда шаблонов Django.
Шаблон
django.template.Template представляет собой скомпилированный шаблон. Шаблоны получаются с помощью Engine.get_template() или Engine.from_string()
Также django.template.backends.django.Template — это тонкий обертка, адаптирующий django.template.Template к общему API шаблонов.
Контекст
django.template.Context хранит метаданные в дополнение к данным контекста. Он передаётся в Template.render() для рендеринга шаблона.
django.template.RequestContext — это подкласс Context, который хранит текущий HttpRequest и выполняет обработчики контекста шаблонов.
В общем API нет эквивалентного понятия. Данные контекста передаются в обычном dict, а текущий HttpRequest передаётся отдельно при необходимости.
Загрузчики
Загрузчики шаблонов отвечают за поиск шаблонов, их загрузку и возврат объектов Template.
Django предоставляет несколько встроенных загрузчиков шаблонов и поддерживает настраиваемые загрузчики шаблонов.
Обработчики контекста
Обработчики контекста — это функции, которые получают текущий HttpRequest в качестве аргумента и возвращают dict данных, которые нужно добавить в контекст рендеринга.
Их основное назначение — добавление общих данных, используемых всеми шаблонами, в контекст без повторения кода в каждом представлении.
Django предоставляет множество встроенных обработчиков контекста. Реализация пользовательского обработчика контекста так же проста, как определение функции.
© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/1.11/topics/templates/