Spec-Zone.ru › Django 5.1

Как создавать пользовательские теги и фильтры шаблонов

Язык шаблонов 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 %} работал. Это функция безопасности: она позволяет размещать код Python для многих библиотек шаблонов на одном хост-машине, не предоставляя доступ ко всем из них для каждой установки Django.

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

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

from django import template

register = template.Library()

В качестве альтернативы, модули тегов шаблонов можно зарегистрировать через аргумент '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. При выводе они экранируются, если автоматическое экранирование включено, и выводятся без изменений в противном случае.
  • Безопасные строки — это строки, помеченные как безопасные для дальнейшего экранирования во время вывода. Любое необходимое экранирование уже выполнено. Они обычно используются для вывода, содержащего необработанный HTML, который должен интерпретироваться на стороне клиента как есть.

    Внутри эти строки имеют тип SafeString. Вы можете проверить их с помощью кода, такого как:

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

Код фильтра шаблона попадает в одну из двух ситуаций:

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

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

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

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

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

    Для того, чтобы пометить вывод как безопасную строку, используйте django.utils.safestring.mark_safe().

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

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

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

    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 или нет. (В последнем случае мы используем функцию тождества как функцию «экранирования».) Функция conditional_escape() похожа на escape(), за исключением того, что она экранирует только входные данные, которые не являются экземпляром SafeData. Если экземпляр SafeData передаётся в conditional_escape(), данные возвращаются без изменений.

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

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

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

Избегание уязвимостей XSS при повторном использовании встроенных фильтров

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

В более старых версиях Django будьте осторожны при повторном использовании встроенных фильтров 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 ""

Когда этот флаг установлен, если первый аргумент вашего фильтра — объект datetime, учитывающий часовой пояс, 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().

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

Далее создайте шаблон, используемый для рендеринга вывода тега. Этот шаблон является фиксированной частью тега: его задаёт автор тега, а не разработчик шаблона. В соответствии с нашим примером, шаблон очень короткий:

<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, которые указывают на главную страницу. Вот как будет выглядеть функция Python:

@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 примера.

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

Краткое описание

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

Когда Django компилирует шаблон, он разбивает исходный текст шаблона на «узлы». Каждый узел является экземпляром 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() обычно должен завершаться без ошибок, особенно в производственной среде. В некоторых случаях, однако, особенно если context.template.engine.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))

Если мы забыли передать текущее значение 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, связанную с context шаблона, который в данный момент отображается. render_context ведет себя как словарь Python и должна использоваться для хранения Node состояния между вызовами метода 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 для ограничения CycleNode информации внутри render_context. В данном шаблоне может быть несколько CycleNodes, поэтому мы должны быть осторожны, чтобы не перезаписывать информацию о состоянии другого узла. Самый простой способ сделать это — всегда использовать 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])

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

Наконец, если вам нужен простой синтаксис для вашего пользовательского тега шаблона, обновляющего контекст, рассмотрите использование сокращения 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() возвращает пустую строку. Всё между {% 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.

© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/5.1/howto/custom-template-tags/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API