Spec-Zone.ru › Django 2.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, который должен интерпретироваться на стороне клиента как есть.

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

    from django.utils.safestring import SafeText
    
    if isinstance(value, SafeText):
        # 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() при необходимости. Когда переменная в конечном итоге будет отображена, на неё повлияет настройка автоматического экранирования, действующая в тот момент, поэтому контент, который должен быть защищён от дальнейшего экранирования, должен быть помечен как таковой.

Кроме того, если ваш тег шаблона создаёт новый контекст для выполнения некоторого подрендеринга, установите атрибут автоматического экранирования в текущее значение контекста. Метод __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/2.2/howto/custom-template-tags/

Spec-Zone.ru

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