Spec-Zone.ru › Django 3.0

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

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

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

Spec-Zone.ru

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