Spec-Zone.ru › Django 1.11

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

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

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

    if isinstance(value, SafeData):
        # 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 или unicode, а вместо того, чтобы пытаться поймать их все, что было бы очень сложно, 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.template.Library.assignment_tag()

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

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

Наша предыдущая функция current_time могла быть записана следующим образом:

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

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

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

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

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

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

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

Когда Django компилирует шаблон, он разбивает исходный текст шаблона на «узлы». Каждый узел — это экземпляр 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/1.11/howto/custom-template-tags/

Spec-Zone.ru

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