Spec-Zone.ru › Django 5.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/5.0/howto/custom-template-tags/

Spec-Zone.ru

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