Spec-Zone.ru › Django 3.2

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

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

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

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

END_OF_DOCUMENT_MARKER

Чтобы убедиться, что ваши теги шаблона потокобезопасны, вы никогда не должны хранить информацию о состоянии в самом узле. Например, 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/3.2/howto/custom-template-tags/

Spec-Zone.ru

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