Собственные шаблонные теги и фильтры

Шаблонизатор Django содержит большое количество встроенных тегов и фильтров. Тем не менее, вам может понадобиться добавить собственный функционал к шаблонам. Вы можете сделать это добавив собственную библиотеку тегов и фильтров используя Python, затем добавить ее в шаблон с помощью тега {% load %}.

Добавление собственной библиотеки

Обычно шаблонные теги и фильтры располагаются в приложении Django. Если они связаны с существующим приложением, это логично расположить его там. Иначе можно добавить их в новое приложение. Если приложение Django добавлено INSTALLED_APPS, все библиотеки тегов, расположенные в определенном модуле приложения, будут доступны для загрузки в шаблонах.

Приложение должно содержать каталог templatetags на том же уровне что и models.py, views.py и др. Если он не существует, создайте его. Не забудьте создать файл __init__.py чтобы каталог мог использоваться как пакет Python.

Сервер для разработки не перезапускается автоматически

После добавления модуля templatetags вам необходимо перезапустить сервер, чтобы использовать теги и фильтры в шаблонах.

Выше теги и фильтры будут находиться в модуле пакета templatetags. Название модуля будет использоваться при загрузке библиотеки в шаблоне, так что убедитесь что оно не совпадает с названиями библиотек других приложений.

Например, если теги/фильтры находятся в файле poll_extras.py, ваше приложение может выглядеть следующим образом:

polls/
    __init__.py
    models.py
    templatetags/
        __init__.py
        poll_extras.py
    views.py

И в шаблоне вы будете использовать:

{% load poll_extras %}

Приложение содержащее собственные теги и фильтры должно быть добавлено в INSTALLED_APPS, чтобы тег {% load %} мог загрузить его. Это сделано в целях безопасности.

Не имеет значение сколько модулей добавлено в пакет templatetags. Помните что тег {% load %} использует название модуля, а не название приложения.

Библиотека тегов должна содержать переменную register равную экземпляру template.Library, в которой регистрируются все определенные теги и фильтры. Так что в начале вашего модуля укажите следующие строки:

from django import template

register = template.Library()
Добавлено в Django 1.9.

Модуль с шаблонными тегами можно также зарегистрировать через аргумент 'libraries' класса DjangoTemplates. Это полезно, если вы хотите изменить название библиотеки тегов. Также вы можете зарегистрировать библиотеку без установки приложения.

За кулисами

Вы можете найти большое количество примеров в исходном коде встроенных тегов и фильтров Django. Они находятся в файлах django/template/defaultfilters.py и django/template/defaulttags.py.

Подробности о теге load читайте в этой документации.

Создание собственного шаблонного фильтра

Фильтры это просто функции Python, которые принимают один или несколько аргументов:

  • Входящее значение – не обязательно строка.

  • Значение аргументов – можно указать значение по умолчанию или вообще не использовать аргументы.

Например, при {{ var|foo:"bar" }} функция фильтра foo будет выполнена со значением переменной var и аргументом "bar".

Исключение, вызванное в шаблонном фильтре, приведет к серверной ошибке. По этому фильтр должен избегать исключений, если можно вернуть какое-то значение по умолчанию. Если же переданное значение явно ведет к багу, лучше вызвать исключение, чем скрыть баг.

Пример фильтра:

def cut(value, arg):
    """Removes all values of arg from the given string"""
    return value.replace(arg, '')

И пример как его использовать:

{{ somevariable|cut:"0" }}

Большинство фильтров не принимают аргументы. Например:

def lower(value): # Only one argument.
    """Converts a string into all lowercase"""
    return value.lower()

Регистрация фильтров

django.template.Library.filter()

Создав функцию фильтра, ее необходимо зарегистрировать в экземпляре Library, чтобы использовать в шаблонах Django:

register.filter('cut', cut)
register.filter('lower', lower)

Метод Library.filter() принимает два аргумента:

  1. Название фильтра – строка.

  2. Функция компиляции – функция Python (не название функции строкой).

Вы можете использовать register.filter() как декоратор:

@register.filter(name='cut')
def cut(value, arg):
    return value.replace(arg, '')

@register.filter
def lower(value):
    return value.lower()

Если вы не укажете аргумент name, как показано во втором примере, Django будет использовать название функции в качестве названия фильтра.

Также register.filter() принимает три именованных аргумента: is_safe, needs_autoescape и expects_localtime. Эти аргументы описан в разделе фильтры и автоматическое экранирование и в разделе фильтры и часовые пояса далее.

Шаблонные фильтры, которые обрабатывают строки

django.template.defaultfilters.stringfilter()

Если вы создали фильтр, который работает только со строками, используйте декоратор stringfilter. Он преобразует объект в строковое значение перед передачей в функцию:

from django import template
from django.template.defaultfilters import stringfilter

register = template.Library()

@register.filter
@stringfilter
def lower(value):
    return value.lower()

В этом случае вы можете передать число в фильтр и это не вызовет исключение AttributeError (так как число не содержит метод lower()).

Фильтры и автоматическое экранирование

Создавая собственный фильтр, учитывайте как он будет работать с политикой автоматического экранирования в Django. Есть три типа строк, которые могу передаваться в коде шаблона.

  • “Сырые” строки – это обычные типы Python str или unicode. При выводе они экранируются при включенном авто-экранировании, иначе – выводятся как есть.

  • Безопасные строки – строки, которые были помечены как безопасные. Указывают на то, что последующее экранирование не требуется. Они обычно используются для строк, которые содержат готовый HTML, которые необходимо отобразить на странице.

    Внутри эти строки представлены типами SafeBytes или SafeText. Эти типа наследуются от базового класса SafeData, таким образом вы можете проверять их следующим образом:

    if isinstance(value, SafeData):
        # Do something with the "safe" string.
        ...
    
  • Строки с пометкой “требуют экранирования”всегда экранируются при выводе, независимо от того находятся они в блоке autoescape или нет. Такие строки экранируются только один раз, независимо от того, включено автоматическое экранирование или нет.

    Внутренне эти строки представлены типами EscapeBytes или EscapeText. Вам не обязательно это знать, можно просто использовать фильтр escape.

При создании фильтра вы можете столкнуться со следующими ситуациями:

  1. Ваш фильтр не добавляет никаких не экранированных HTML-символов (<, >, ', " or &) в результат. В таком случае вы можете полностью положиться на политику автоматического экранирования Django. Для этого передайте параметр is_safe с значением True при регистрации функции фильтра:

    @register.filter(is_safe=True)
    def myfilter(value):
        return value
    

    Этот параметр указывает Django что фильтр никак не изменяет “безопасность” переданной строки. То есть, если передать в фильтр “безопасную” строку, результат также будет “безопасным” для Django, если же передать “небезопасную” строку, Django автоматически экранирует результат фильтра.

    Другими словами можно сказать “этот фильтр безопасный – он никаким образом не добавляет небезопасный HTML в результат.”

    Причина использования параметра is_safe состоит в том, что большинство операций со строками превращает объект SafeData обратно в обычный объект str или unicode и чтобы не обрабатывать все эти ситуации самостоятельно, что может быть не просто, Django самостоятельно следит за изменениями.

    Например, у вас есть фильтр, который добавляет xx к концу переданного значения. Так как он не добавляет небезопасных HTML-символов в результат (кроме тех, которые присутствуют в переданном значении), вы должные пометить его с параметром is_safe:

    @register.filter(is_safe=True)
    def add_xx(value):
        return '%sxx' % value
    

    Если фильтр используется в шаблоне с включенным автоматическим экранированием, Django выполнит экранирование результата если входящие данные не были отмечены как “безопасные”.

    По умолчанию is_safe равен False.

    Будьте внимательны определяя безопасен ваш фильтр или нет. Если вы удаляете символы, вы можете случайно оставить открытые HTML теги или сущности(entities) в результате. Например, при удалении > из входящих данных <a> может превратиться в <a, который должен быть экранирован. Аналогично, удаление точки с запятой (;) может превратить &amp; в &amp, что не является правильной HTML-сущностью и должно быть экранировано. Большинство случаев будут не такими сложными, но вы должны быть внимательными.

    Параметр is_safe принуждает фильтр вернуть строку. Если ваш фильтр возвращает булево значение или не строку, использование is_safe может привести к непредвиденным последствиям (например, конвертирование False в строку ‘False’).

  2. Фильтр самостоятельно заботится об экранировании результата. Это необходимо если вы добавляете новый HTML в результат. В таком случае результат должен быть помечен как безопасный чтобы избежать последующего экранирования.

    Для этого используйте функцию django.utils.safestring.mark_safe().

    Но будьте осторожны. Необходимо не просто пометить результат как безопасный. Вы должны убедиться что результат действительно безопасен независимо от того, включено автоматическое экранирование или нет. Идея в том, чтобы фильтр правильно работал как при включенном автоматическом экранировании, так и выключенном.

    Для того, чтобы фильтр знал включено ли автоматическое экранирование, передайте параметр needs_autoescape со значением True при регистрации функций фильтра. (По умолчанию значение равно False). Этот параметр указывает Django что необходимо передать именованный аргумент autoescape при вызове функции фильтра, который равен True, если включено автоматическое экранирование, иначе False. Рекомендуем по умолчанию указать True в autoescape, чтобы при вызове функции в коде, экранирование было включено.

    Например, давайте создадим фильтр, который выделяет первый символ строки:

    from django import template
    from django.utils.html import conditional_escape
    from django.utils.safestring import mark_safe
    
    register = template.Library()
    
    @register.filter(needs_autoescape=True)
    def initial_letter_filter(text, autoescape=True):
        first, other = text[0], text[1:]
        if autoescape:
            esc = conditional_escape
        else:
            esc = lambda x: x
        result = '<strong>%s</strong>%s' % (esc(first), esc(other))
        return mark_safe(result)
    

    Параметр needs_autoescape и аргумент autoescape информируют фильтр о том, было ли включено автоматическое экранирование при вызове фильтра. Аргумент autoescape указывает необходимо ли использовать django.utils.html.conditional_escape для входящих данных. (В нашем примере мы использовали его для определения функции “escape”.) Функция conditional_escape() как и escape(), но использует экранирование только для не безопасных(SafeData) строк. Если передать объект SafeData функция conditional_escape() вернет его без изменений.

    Также мы пометили результат как безопасный и он будет вставлен непосредственно в шаблон без повторного экранирования.

    В этом случае нет необходимости беспокоиться о параметре is_safe. Так как вы самостоятельно учитываете автоматическое экранирование, параметр is_safe ничего не изменит.

Предупреждение

Защита от XSS уязвимостей при использовании встроенных фильтров.

Изменено в Django 1.8.

Фильтры Django используют autoescape=True, чтобы избежать XSS уязвимостей.

В предыдущих версиях Django autoescape равен None по умолчанию, будьте осторожны при использовании фильтров в коде. Вам необходимо передать autoescape=True, чтобы активировать экранирование.

Например, если вы хотите написать фильтр urlize_and_linebreaks, который использует фильтры urlize и linebreaksbr, он будет выглядеть следующим образом:

from django.template.defaultfilters import linebreaksbr, urlize

@register.filter(needs_autoescape=True)
def urlize_and_linebreaks(text, autoescape=True):
    return linebreaksbr(
        urlize(text, autoescape=autoescape),
        autoescape=autoescape
    )

Тогда:

{{ comment|urlize_and_linebreaks }}

можно использовать вместо:

{{ comment|urlize|linebreaksbr }}

Фильтры и временные зоны

Если вы создаете фильтр, который обрабатывает объекты datetime, скорее всего вы будете использовать параметр expects_localtime со значением True:

@register.filter(expects_localtime=True)
def businesshours(value):
    try:
        return 9 <= value.hour < 17
    except AttributeError:
        return ''

Если этот флаг установлен и первый аргумент вашего фильтра является значением времени с часовым поясом, Django преобразует это значение в текущий часовой пояс перед тем как передать в фильтр, в соответствии с правилами преобразования часовых поясов в шаблонах.

Создание собственного шаблонного тега

Теги сложнее чем фильтры, они позволяют делать что угодно. Django предоставляет инструменты, которые упрощают создания различных тегов. Первым делом мы изучим их, затем узнаем как создать тег с нуля.

Простые теги

django.template.Library.simple_tag()

Большинство тегов принимают определенное количество аргументов – строки или переменные шаблона – и возвращают строку после обработки аргументов. Например, тег current_time принимает строку с форматом и возвращает время строкой в этом формате.

Для создания подобных тегов, Django предоставляет функцию simple_tag. Эта функция, которая является методом django.template.Library, принимает функцию принимающую любое количество аргументов, оборачивает функцией render и регистрирует в системе шаблонов.

Функцию current_time можно переписать следующим образом:

import datetime
from django import template

register = template.Library()

@register.simple_tag
def current_time(format_string):
    return datetime.datetime.now().strftime(format_string)

Несколько вещей которые следует помнить о функции simple_tag:

  • Проверка количества обязательных аргументов и др. выполняется до вызова функции, вам не нужно этого делать.

  • Кавычки вокруг строк уже удалены, аргумент будет содержать готовую строку.

  • Если аргумент является переменной шаблона, наша функция получит ее значение.

В отличии от других утилит тегов, simple_tag обрабатывает результат функцией conditional_escape(), если контекст шаблона в режиме автоматического экранирования, чтобы убедиться в правильности HTML и защитить вас от XSS атак.

Если экранирование не нужно, вы можете использовать функцию mark_safe(), если вы абсолютно уверены, что ваш код не содержит XSS уязвимостей. Для создание небольших кусков HTML настоятельно рекомендуется использовать format_html() вместо mark_safe().

Изменено в Django 1.9:

Было добавлено автоматическое экранирование для simple_tag.

Если тегу необходим текущий контекст, используйте параметр takes_context при регистрации тега:

@register.simple_tag(takes_context=True)
def current_time(context, format_string):
    timezone = context['timezone']
    return your_get_current_time_method(timezone, format_string)

Заметим, что первый параметр должен называться context.

Подробности о параметре takes_context смотрите в разделе о включающих тегах.

Если вам нужно изменить название тега, передайте его параметром:

register.simple_tag(lambda x: x - 1, name='minusone')

@register.simple_tag(name='minustwo')
def some_function(value):
    return value - 2

Теги, зарегистрированные через simple_tag могут принимать любое количество позиционных или именованных аргументов. Например:

@register.simple_tag
def my_tag(a, b, *args, **kwargs):
    warning = kwargs['warning']
    profile = kwargs['profile']
    ...
    return ...

Теперь в тег можно передать любое количество позиционных аргументов, разделенных пробелами. Как и в Python, значения для именованных аргументов можно указать, используя знак “=” после именованных аргументов. Например:

{% my_tag 123 "abcd" book.title warning=message|lower profile=user.profile %}
Добавлено в Django 1.9.

Вы можете добавить результат в переменную шаблона, используя аргумент as и название переменной, и использовать его при необходимости:

{% get_current_time "%Y-%m-%d %I:%M %p" as the_time %}
<p>The time is {{ the_time }}.</p>

Включающие теги

django.template.Library.inclusion_tag()

Еще один тип тегов – это теги, которые выполняют другой шаблон и показывают результат. Например, интерфейс администратора Django использует включающий тег для отображения кнопок под формой на страницах добавления/редактирования объектов. Эти кнопки выглядят всегда одинаково, но ссылки зависят от текущего объекта – небольшой шаблон, который выполняется с данными из текущего объекта, удобно использовать в данном случае. (В приложении администратора это тег submit_row.)

Такие теги называются “включающие теги”.

Лучше пояснить на примере. Давайте создадим тег, который выводит список вариантов ответов для объекта модели Poll, которая использовалась в учебнике. Мы будем использовать его следующим образом:

{% show_results poll %}

...результат будет выглядеть приблизительно следующим образом:

<ul>
  <li>First choice</li>
  <li>Second choice</li>
  <li>Third choice</li>
</ul>

Первым делом, создадим функцию, которая принимает аргумент и возвращает словарь с данными. Заметим, что все что нам нужно, это вернуть словарь и ничего более сложного. Он будет использоваться как контекст для включаемого фрагмента шаблона. Например:

def show_results(poll):
    choices = poll.choice_set.all()
    return {'choices': choices}

Теперь создадим шаблон, который будет использоваться для генерации результата. Этот шаблон полностью относится к тегу: создатель тега определяет его, не создатель шаблонов(template designer). Для нашего примера шаблон будет очень простым:

<ul>
{% for choice in choices %}
    <li> {{ choice }} </li>
{% endfor %}
</ul>

Теперь создадим и зарегистрируем тег, используя метод inclusion_tag() объекта Library. Для нашего примера, если шаблон тега называется results.html, мы зарегистрируем тег следующим образом:

# Here, register is a django.template.Library instance, as before
@register.inclusion_tag('results.html')
def show_results(poll):
    ...

Также можно зарегистрировать включающий тег используя экземпляр django.template.Template:

from django.template.loader import get_template
t = get_template('results.html')
register.inclusion_tag(t)(show_results)

...при создании функции.

В некоторых случаях тег может требовать большого количества параметров. Может быть проблематично запомнить все параметры и их порядок. Чтобы решить эту проблему Django предоставляет параметр takes_context для включающего тега. Если указать takes_context при создании тега, тег не будет содержать обязательные аргументы, а функция Python будет принимать один аргумент – контекст текущего шаблона.

Например, предположим вы создаете тег, который будет использоваться в шаблонах с контекстом всегда содержащим переменные home_link и home_title. Вот как может выглядеть такой тег:

@register.inclusion_tag('link.html', takes_context=True)
def jump_link(context):
    return {
        'link': context['home_link'],
        'title': context['home_title'],
    }

Заметим, что первый параметр должен называться context.

При вызове register.inclusion_tag() мы указали takes_context=True и название включаемого шаблона. Вот как может выглядеть шаблон link.html:

Jump directly to <a href="{{ link }}">{{ title }}</a>.

Для использования тега необходимо загрузить библиотеку тегов и вызвать тег без аргументов:

{% jump_link %}

Заметим, что при использовании takes_context=True необязательно передавать аргументы. Тег будет иметь доступ ко всему контексту шаблона.

Параметр takes_context по умолчанию равен False. Если он равен True, в тег будет передан объект контекста.

inclusion_tag может принимать любое количество позиционных и именованных аргументов. Например:

@register.inclusion_tag('my_template.html')
def my_tag(a, b, *args, **kwargs):
    warning = kwargs['warning']
    profile = kwargs['profile']
    ...
    return ...

Теперь в тег можно передать любое количество позиционных аргументов, разделенных пробелами. Как и в Python, значения для именованных аргументов можно указать, используя знак “=” после именованных аргументов. Например:

{% my_tag 123 "abcd" book.title warning=message|lower profile=user.profile %}

Присваивающий тег

django.template.Library.assignment_tag()

Не рекомендуется, начиная с версии 1.9: simple_tag теперь может сохранять результат в переменной шаблона.

Для простого создания тегов, которые добавляют переменную в контекст, Django предоставляет функцию assignment_tag. Эта функция работает так же, как и simple_tag(), за исключением, что она добавляет результат в контекст, а не возвращает его.

Функцию current_time можно переписать следующим образом:

@register.assignment_tag
def get_current_time(format_string):
    return datetime.datetime.now().strftime(format_string)

Вы можете добавить результат в переменную шаблона, используя аргумент as и название переменной, и использовать его при необходимости:

{% get_current_time "%Y-%m-%d %I:%M %p" as the_time %}
<p>The time is {{ the_time }}.</p>

Создание собственного шаблонного тега

Иногда не достаточно базовых инструментов для создания тегов. Не волнуйтесь, Django предоставляет полный доступ к внутренностям шаблонизатора, что позволяет создать свой тег с нуля.

Краткий обзор

Система шаблонов работает в два этапа: компиляция и выполнение. Создавая собственный тег вы определяете как выполняется компиляция и выполнение тега.

Когда Django компилирует шаблон, содержимое шаблона разбивается на “узлы” (nodes). Каждый узел это экземпляр django.template.Node с методом render(). Откомпилированный шаблон это просто список объектов Node. Когда вы вызываете метод render() откомпилированного объекта шаблона, шаблон просто вызывает render() для каждого объекта Node в списке узлов с переданным контекстом. Результаты объединяются для получения окончательного результата.

Таким образом, создавая собственный тег, вы указываете как “сырой” тег шаблона конвертируется в объект Node (функцию компиляции) и что делает метод render().

Создание функции компиляции

Для каждого тега, с которым сталкивается парсер шаблона, вызывается его функция Python с содержимым тега и объектом парсера. Эта функция должна вернуть экземпляр Node.

Например, давайте создадим тег, {% current_time %}, который отображает текущую дату и время, отформатированные в соответствии с переданным параметром с синтаксисом аналогичным strftime(). Первым делом следует определиться с синтаксисом тега. В нашем случае тег будет использоваться следующим образом:

<p>The time is {% current_time "%Y-%m-%d %I:%M %p" %}.</p>

Парсер функции должен получить параметр и вернуть объект Node:

from django import template

def do_current_time(parser, token):
    try:
        # split_contents() knows not to split quoted strings.
        tag_name, format_string = token.split_contents()
    except ValueError:
        raise template.TemplateSyntaxError(
            "%r tag requires a single argument" % token.contents.split()[0]
        )
    if not (format_string[0] == format_string[-1] and format_string[0] in ('"', "'")):
        raise template.TemplateSyntaxError(
            "%r tag's argument should be in quotes" % tag_name
        )
    return CurrentTimeNode(format_string[1:-1])

Заметки:

  • parser – парсер шаблона. Он нам не нужен в данном примере.

  • token.contents – содержимое тега. В нашем примере это 'current_time "%Y-%m-%d %I:%M %p"'.

  • Метод token.split_contents() разбивает аргументы разделенные пробелами при это не разбивая строки выделенные кавычками. Более простой метод token.contents.split() может быть не таким полезным и надежным так как разбивает по всем пробелам, включая пробелы в кавычках. Лучше всегда использовать token.split_contents().

  • Эта функция может вызвать исключение django.template.TemplateSyntaxError в случае синтаксической ошибки при использовании вашего тега.

  • Исключение TemplateSyntaxError использует переменную tag_name. Не вписывайте название тега в сообщение ошибки, потому что это привязывает название тега к функции. token.contents.split()[0] ‘’всегда’’ содержит название тега – даже если тег не содержит аргументы.

  • Функция возвращает экземпляр CurrentTimeNode передавая в конструктор необходимую информацию с тега. В нашем примере передается "%Y-%m-%d %I:%M %p". Кавычки удаляются с помощью format_string[1:-1].

  • Парсер – очень низкоуровневый. Разработчики Django экспериментировали с созданием различных микро-фреймверков поверх системы парсинга, используя техники, такие как грамматика EBNF, но эти эксперименты делали систему шаблонов медленной. Парсер низкоуровневый, так как это делает его быстрым.

Реализация выполнения тега

Следующим этапом мы создаем подкласс Node с методом render().

В продолжение нашего примера создадим класс CurrentTimeNode:

import datetime
from django import template

class CurrentTimeNode(template.Node):
    def __init__(self, format_string):
        self.format_string = format_string

    def render(self, context):
        return datetime.datetime.now().strftime(self.format_string)

Заметки:

  • __init__() принимает аргумент format_string из do_current_time(). Всегда передавайте параметры в Node через __init__().

  • Метод render() выполняет основную работу.

  • render() не должен вызывать исключений, особенно на боевом сервере. Однако, в некоторых случаях, особенно при TEMPLATE_DEBUG равном True, метод может вызывать исключения для упрощения отладки. Например, некоторые встроенные теги вызывают django.template.TemplateSyntaxError, если передать неверное количество или тип аргументов.

Разделение компиляции и выполнения эффективно так как позволяет выполнить шаблон с несколькими контекстами без надобности выполнять парсинг каждый раз.

Работа с автоматическим экранированием

Вывод тега не экранируется(за исключением simple_tag()). Однако, есть несколько вещей которые следует помнить.

Если метод render() добавляет переменную в контекст (вместо того, чтобы вернуть строку), он должен пометить ее как безопасную используя функцию mark_safe(), если это необходимо. В конечном итоге к переменной будет применяться автоматическое экранирование при выводе в шаблоне, так что необходимо пометить ее как безопасную чтобы избежать повторного экранирования значения.

Если тег создает новый контекст, необходимо установить параметр автоматического экранирования со значением текущего контекста. Метод __init__ класса Context принимает аргумент autoescape, который вы можете использовать. Например:

from django.template import Context

def render(self, context):
    # ...
    new_context = Context({'var': obj}, autoescape=context.autoescape)
    # ... Do something with new_context ...

Это не совсем обычная ситуация, но может быть полезно если вы самостоятельно выполняете шаблон. Например:

def render(self, context):
    t = context.template.engine.get_template('small_fragment.html')
    return t.render(Context({'var': obj}, autoescape=context.autoescape))
Изменено в Django 1.8:

В Django 1.8 был добавлен атрибут template объектам Context. Вместо django.template.loader.get_template() следует использовать context.template.engine.get_template, потому что первый возвращает обертку с методом render, который не принимает Context.

Если бы мы не передали значение context.autoescape в новый Context, результат всегда экранировался бы, что может быть неуместным при использовании тега в блоке {% autoescape off %}.

Учитываем потокобезопасность

После парсинга тега и создания узла его метод render может быть вызван любое количество раз. Так как Django может выполняться в мультипоточном окружении, один узел может выполняться с несколькими контекстами для различных запросов. По этом важно удостовериться что ваши шаблонные теги являются потокобезопасными.

Чтобы ваш шаблонный тег был потокобезопасный, вы не должны сохранять информацию о состоянии в узле. Например, Django предоставляет тег cycle, который переключается между аргументами при каждом вызове:

{% for o in some_list %}
    <tr class="{% cycle 'row1' 'row2' %}">
        ...
    </tr>
{% endfor %}

Реализация CycleNode могла бы выглядеть следующим образом:

import itertools
from django import template

class CycleNode(template.Node):
    def __init__(self, cyclevars):
        self.cycle_iter = itertools.cycle(cyclevars)

    def render(self, context):
        return next(self.cycle_iter)

Однако, предположим что одновременно выполняется два экземпляра шаблона представленного выше:

  1. Поток 1 выполняет первую итерацию по циклу, CycleNode.render() возвращает ‘row1’

  2. Поток 2 выполняет первую итерацию по циклу, CycleNode.render() возвращает ‘row2’

  3. Поток 1 выполняет вторую итерацию по циклу, CycleNode.render() возвращает ‘row1’

  4. Поток 2 выполняет вторую итерацию по циклу, CycleNode.render() возвращает ‘row2’

CycleNode работает, но итерация происходит глобально. Так как Поток 1 и Поток 2 связаны, они используют одни значения. Это точно не то, что вам нужно!

Для решения этой проблемы Django предоставляет render_context в контексте текущего шаблона. render_context работает как и словарь в Python и должен использоваться для хранения состояния узлов между вызовами метода render.

Давайте перепишем CycleNode чтобы использовать render_context:

class CycleNode(template.Node):
    def __init__(self, cyclevars):
        self.cyclevars = cyclevars

    def render(self, context):
        if self not in context.render_context:
            context.render_context[self] = itertools.cycle(self.cyclevars)
        cycle_iter = context.render_context[self]
        return next(cycle_iter)

Заметим, что вполне безопасно сохранять в атрибутах объекта Node информацию, которая не изменяется. В случае CycleNode, параметр cyclevars не изменяется после создания экземпляра Node, и нет необходимости хранить его в render_context. Но информация, которая относится к конкретному шаблону, например текущая итерация узла CycleNode, должна сохраняться в render_context.

Примечание

Обратите внимание как мы используем self для привязки состояния к текущему узлу в render_context. В шаблоне может быть несколько CycleNode, и важно не нарушить состояние других узлов. Самый просто способ это использовать self в качестве ключа в render_context. Если вам необходимо хранить несколько переменных, используйте в render_context[self] словарь.

Регистрация тега

Теперь зарегистрируем тег в экземпляре Library вашего модуля, как описано выше. Например:

register.tag('current_time', do_current_time)

Метод tag() принимает два аргумента:

  1. Название шаблонного тега – строкой. Если параметр не указан, используется название функции.

  2. Функция компиляции – функция Python (не название функции строкой).

Как и для регистрации фильтра, можно использовать как декоратор:

@register.tag(name="current_time")
def do_current_time(parser, token):
    ...

@register.tag
def shout(parser, token):
    ...

Если не указать параметр name, как во втором примере, Django будет использовать название функции в качестве названия тега.

Передача переменных шаблона в тег

Хоть вы и можете передать любое количество аргументов в шаблонный тег используя token.split_contents(), все аргументы передаются как строка. Чтобы передать значение переменной шаблона, необходимо немного усложнить код.

Тег из примера выше форматирует текущее время и возвращает строку. Предположим вы хотите передать объект DateTimeField и отформатировать это значение:

<p>This post was last updated at {% format_time blog_entry.date_updated "%Y-%m-%d %I:%M %p" %}.</p>

token.split_contents() вернет три значения:

  1. Название тега format_time.

  2. Строку 'blog_entry.date_updated' (без кавычек).

  3. Строку форматирования '"%Y-%m-%d %I:%M %p"'. Значение из split_contents() будет содержать кавычки для таких переменных.

Теперь ваш тег будет выглядеть следующим образом:

from django import template

def do_format_time(parser, token):
    try:
        # split_contents() knows not to split quoted strings.
        tag_name, date_to_be_formatted, format_string = token.split_contents()
    except ValueError:
        raise template.TemplateSyntaxError(
            "%r tag requires exactly two arguments" % token.contents.split()[0]
        )
    if not (format_string[0] == format_string[-1] and format_string[0] in ('"', "'")):
        raise template.TemplateSyntaxError(
            "%r tag's argument should be in quotes" % tag_name
        )
    return FormatTimeNode(date_to_be_formatted, format_string[1:-1])

Теперь вам следует изменить метод render узла чтобы получить значение атрибута date_updated объекта blog_entry. Это может быть выполнено с использованием класса Variable() из django.template.

Чтобы использовать класс Variable, создайте экземпляр указав название переменной, потом вызовите variable.resolve(context). Например:

class FormatTimeNode(template.Node):
    def __init__(self, date_to_be_formatted, format_string):
        self.date_to_be_formatted = template.Variable(date_to_be_formatted)
        self.format_string = format_string

    def render(self, context):
        try:
            actual_date = self.date_to_be_formatted.resolve(context)
            return actual_date.strftime(self.format_string)
        except template.VariableDoesNotExist:
            return ''

Будет вызвано исключение VariableDoesNotExist если невозможно найти значение переменной в текущем контексте.

Добавление переменной в контекст

Предыдущие примеры тегов просто выводят значение. Более гибкий способ - это добавить значение в переменную контекста вместо вывода результата. Таким образом автор шаблона может использовать результат выполнения тега несколько раз.

Чтобы добавить переменную в контекст, просто добавьте значение в контекст как в словарь в методе render(). Вот обновленная версия CurrentTimeNode, которая устанавливает переменную current_time вместо вывода результата:

import datetime
from django import template

class CurrentTimeNode2(template.Node):
    def __init__(self, format_string):
        self.format_string = format_string
    def render(self, context):
        context['current_time'] = datetime.datetime.now().strftime(self.format_string)
        return ''

Заметим, что render() возвращает пустую строку. render() всегда должен возвращать строку. Если все, что делает тег, это добавление переменной в контекст, метод render() должен вернуть пустую строку.

Вот как вы можете использовать новую версию тега:

{% current_time "%Y-%M-%d %I:%M %p" %}<p>The time is {{ current_time }}.</p>

Область видимости переменной в контексте

Любая переменная, добавленная в контекст будет доступна только в блоке(block) шаблона, в котором она была добавлена. Так сделано намерено, чтобы переменные не конфликтовали с контекстом другого блока.

Но есть одна проблема в CurrentTimeNode2: название переменной current_time “вшито” в тег. Это означает, что вы должны убедиться, что переменная {{ current_time }} не используется в шаблоне, потому что {% current_time %} перезапишет ее. Правильное решение – позволить указывать название переменной при вызове тега:

{% current_time "%Y-%M-%d %I:%M %p" as my_current_time %}
<p>The current time is {{ my_current_time }}.</p>

Чтобы это сделать вам нужно изменить код функции компиляции и подкласса Node:

import re

class CurrentTimeNode3(template.Node):
    def __init__(self, format_string, var_name):
        self.format_string = format_string
        self.var_name = var_name
    def render(self, context):
        context[self.var_name] = datetime.datetime.now().strftime(self.format_string)
        return ''

def do_current_time(parser, token):
    # This version uses a regular expression to parse tag contents.
    try:
        # Splitting by None == splitting by spaces.
        tag_name, arg = token.contents.split(None, 1)
    except ValueError:
        raise template.TemplateSyntaxError(
            "%r tag requires arguments" % token.contents.split()[0]
        )
    m = re.search(r'(.*?) as (\w+)', arg)
    if not m:
        raise template.TemplateSyntaxError("%r tag had invalid arguments" % tag_name)
    format_string, var_name = m.groups()
    if not (format_string[0] == format_string[-1] and format_string[0] in ('"', "'")):
        raise template.TemplateSyntaxError(
            "%r tag's argument should be in quotes" % tag_name
        )
    return CurrentTimeNode3(format_string[1:-1], var_name)

Разница в том, что do_current_time() получает формат строки и название переменной и передает в конструктор CurrentTimeNode3.

Если вам нужно только добавить переменную в контекст, воспользуйтесь присваивающий тег (assignment tag).

Создание блочного тега

Шаблонные теги могут работать вместе. Например, встроенный тег {% comment %} скрывает содержимое до тега {% endcomment %}. Чтобы создать подобный тег, используйте parser.parse() в функции компиляции тега.

Вот простая реализация тега {% comment %}:

def do_comment(parser, token):
    nodelist = parser.parse(('endcomment',))
    parser.delete_first_token()
    return CommentNode()

class CommentNode(template.Node):
    def render(self, context):
        return ''

Примечание

Реализация {% comment %} немного отличается от нашего примера, позволяя использовать неправильные теги между {% comment %} и {% endcomment %}. Для этого используется parser.skip_past('endcomment') вместо parser.parse(('endcomment',)) перед parser.delete_first_token(), такой вариант не генерирует список узлов.

parser.parse() принимает кортеж названий тегов для “парсинга, пока они не встретятся”. Функция вернет объект django.template.NodeList, который является списком объектов Node встреченных ‘’до’’ любого из тегов указанных в кортеже.

В "nodelist = parser.parse(('endcomment',))" из нашего примера, nodelist – это список всех узлов встреченных между {% comment %} и {% endcomment %}, не включая {% comment %} и {% endcomment %}.

После вызова parser.parse() парсер не “обрабатывает” тег {% endcomment %}, поэтому необходимо вызвать parser.delete_first_token().

CommentNode.render() просто возвращает пустую строку. Все между {% comment %} и {% endcomment %} игнорируется.

Обработка блочного тега с сохранением содержимого

В примере выше, do_comment() игнорирует содержимое между {% comment %} и {% endcomment %}. Вместо этого можно выполнить какие-либо операции над содержимым блочного тега.

Например, у нас есть тег {% upper %}, который преобразует содержимое до тега {% endupper %} в верхний регистр.

Пример использования:

{% upper %}This will appear in uppercase, {{ your_name }}.{% endupper %}

Как и в предыдущем примере мы будем использовать parser.parse(). Но в этот раз полученный nodelist передадим в Node:

def do_upper(parser, token):
    nodelist = parser.parse(('endupper',))
    parser.delete_first_token()
    return UpperNode(nodelist)

class UpperNode(template.Node):
    def __init__(self, nodelist):
        self.nodelist = nodelist
    def render(self, context):
        output = self.nodelist.render(context)
        return output.upper()

Новым здесь является вызов self.nodelist.render(context) в UpperNode.render().

Более сложные примеры ищите в исходном коде реализации {% for %} в django/template/defaulttags.py и {% if %} в django/template/smartif.py.