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

Шаблонизатор 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()

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

За кулисами

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

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

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

Custom filters are Python functions that take one or two arguments:

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

Например, при {{ 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" }}

Most filters don’t take arguments. In this case, leave the argument out of your function:

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. При выводе они экранируются при включенном авто-экранировании, иначе – выводятся как есть.

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

    Internally, these strings are of type SafeString. You can test for them using code like:

    from django.utils.safestring import SafeString
    
    if isinstance(value, SafeString):
        # Do something with the "safe" string.
        ...
    

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

  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 и, чтобы не обрабатывать все эти ситуации самостоятельно, что может быть не просто, 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)
    

    The needs_autoescape flag and the autoescape keyword argument mean that our function will know whether automatic escaping is in effect when the filter is called. We use autoescape to decide whether the input data needs to be passed through django.utils.html.conditional_escape or not. (In the latter case, we use the identity function as the «escape» function.) The conditional_escape() function is like escape() except it only escapes input that is not a SafeData instance. If a SafeData instance is passed to conditional_escape(), the data is returned unchanged.

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

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

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

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

Фильтры 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:

  • Проверка количества обязательных аргументов и др. выполняется до вызова функции, вам не нужно этого делать.
  • The quotes around the argument (if any) have already been stripped away, so we receive a plain string.
  • Если аргумент является переменной шаблона, наша функция получит ее значение.

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

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

Если тегу необходим текущий контекст, используйте параметр 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 %}

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

{% 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}

Next, create the template used to render the tag’s output. This template is a fixed feature of the tag: the tag writer specifies it, not the template designer. Following our example, the template is very short:

<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 предоставляет полный доступ к внутренностям шаблонизатора, что позволяет создать свой тег с нуля.

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

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

When Django compiles a template, it splits the raw template text into „“nodes““. Each node is an instance of django.template.Node and has a render() method. A compiled template is a list of Node objects. When you call render() on a compiled template object, the template calls render() on each Node in its node list, with the given context. The results are all concatenated together to form the output of the template.

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

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

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

For example, let’s write a full implementation of our template tag, {% current_time %}, that displays the current date/time, formatted according to a parameter given in the tag, in strftime() syntax. It’s a good idea to decide the tag syntax before anything else. In our case, let’s say the tag should be used like this:

<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] „“всегда““ содержит название тега – даже если тег не содержит аргументы.
  • The function returns a CurrentTimeNode with everything the node needs to know about this tag. In this case, it passes the argument – "%Y-%m-%d %I:%M %p". The leading and trailing quotes from the template tag are removed in 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()). Однако, есть несколько вещей которые следует помнить.

If the render() method of your template tag stores the result in a context variable (rather than returning the result in a string), it should take care to call mark_safe() if appropriate. When the variable is ultimately rendered, it will be affected by the auto-escape setting in effect at the time, so content that should be safe from further escaping needs to be marked as such.

Если тег создает новый контекст, необходимо установить параметр автоматического экранирования со значением текущего контекста. Метод __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))

Если бы мы не передали значение 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“

The CycleNode is iterating, but it’s iterating globally. As far as Thread 1 and Thread 2 are concerned, it’s always returning the same value. This is not what we want!

Для решения этой проблемы 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] словарь.

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

Finally, register the tag with your module’s Library instance, as explained in writing custom template tags above. Example:

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.

To use the Variable class, instantiate it with the name of the variable to be resolved, and then call variable.resolve(context). So, for example:

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 если невозможно найти значение переменной в текущем контексте.

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

The above examples output a value. Generally, it’s more flexible if your template tags set template variables instead of outputting values. That way, template authors can reuse the values that your template tags create.

To set a variable in the context, use dictionary assignment on the context object in the render() method. Here’s an updated version of CurrentTimeNode that sets a template variable current_time instead of outputting it:

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.

В конце концов, если вам необходимо просто добавить переменную в контекст, обратите внимание на simple_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() returns an empty string. Anything between {% comment %} and {% endcomment %} is ignored.

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

В примере выше, 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.