Spec-Zone.ru › Django 4.2

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

Язык шаблонов Django поставляется с широким набором встроенных тегов и фильтров, предназначенных для удовлетворения потребностей вашей приложения в логике представления. Тем не менее, вам может потребоваться функциональность, которая не покрывается набором основных примитивов шаблонов. Вы можете расширить движок шаблонов, определив пользовательские теги и фильтры с помощью Python, а затем сделать их доступными для ваших шаблонов с помощью тега {% load %}.

Структура кода

Самое распространенное место для определения пользовательских тегов и фильтров шаблонов — внутри приложения Django. Если они относятся к существующему приложению, имеет смысл объединить их там; в противном случае их можно добавить в новое приложение. Когда приложение Django добавляется в INSTALLED_APPS, любые теги, которые оно определяет в стандартном расположении, описанном ниже, автоматически становятся доступными для загрузки в шаблонах.

Приложение должно содержать каталог templatetags, на том же уровне, что и models.py, views.py, и т. д. Если его еще нет, создайте его — не забудьте файл __init__.py, чтобы убедиться, что каталог обрабатывается как пакет Python.

Сервер разработки не будет автоматически перезапускаться

После добавления модуля templatetags вам необходимо перезапустить сервер, прежде чем вы сможете использовать теги или фильтры в шаблонах.

Ваши пользовательские теги и фильтры будут храниться в модуле внутри каталога templatetags. Имя файла модуля — это имя, которое вы будете использовать для загрузки тегов позже, поэтому будьте внимательны, выбирая имя, которое не будет конфликтовать с пользовательскими тегами и фильтрами в другом приложении.

Например, если ваши пользовательские теги/фильтры находятся в файле poll_extras.py, структура вашего приложения может выглядеть так:

polls/
    __init__.py
    models.py
    templatetags/
        __init__.py
        poll_extras.py
    views.py

И в вашем шаблоне вы бы использовали следующее:

{% load poll_extras %}

Приложение, содержащее пользовательские теги, должно быть в INSTALLED_APPS, чтобы тег {% load %} работал. Это мера безопасности: она позволяет размещать код Python для многих библиотек шаблонов на одном хост-компьютере, не предоставляя доступ ко всем из них для каждой установки Django.

Нет ограничений на количество модулей, которые вы можете разместить в пакете templatetags. Просто помните, что оператор {% load %} загрузит теги/фильтры для данного имени Python-модуля, а не для имени приложения.

Чтобы быть действительной библиотекой тегов, модуль должен содержать переменную уровня модуля с именем register, которая является экземпляром template.Library, в котором регистрируются все теги и фильтры. Поэтому, в начале вашего модуля, поместите следующее:

from django import template

register = template.Library()

В качестве альтернативы, модули тегов шаблонов можно зарегистрировать с помощью аргумента 'libraries' для DjangoTemplates. Это полезно, если вы хотите использовать другое имя вместо имени модуля тега шаблона при загрузке тегов шаблона. Это также позволяет регистрировать теги без установки приложения.

За кулисами

Для множества примеров прочитайте исходный код стандартных фильтров и тегов Django. Они находятся в django/template/defaultfilters.py и django/template/defaulttags.py соответственно.

Для получения дополнительной информации о теге load, прочитайте его документацию.

Написание пользовательских фильтров шаблонов

Пользовательские фильтры — это функции Python, принимающие один или два аргумента:

  • Значение переменной (вход) — не обязательно строка.
  • Значение аргумента — оно может иметь значение по умолчанию или быть опущено совсем.

Например, в фильтре {{ var|foo:"bar" }}, фильтру foo будет передан параметр var и аргумент "bar".

Поскольку язык шаблонов не предоставляет обработки исключений, любое исключение, поднятое фильтром шаблона, будет отображено как ошибка сервера. Таким образом, функции фильтров должны избегать возбуждения исключений, если существует разумное значение по умолчанию для возврата. В случае входных данных, представляющих явную ошибку в шаблоне, возбуждение исключения может быть предпочтительнее, чем молчаливое завершение, которое скрывает ошибку.

Вот пример определения фильтра:

def cut(value, arg):
    """Removes all values of arg from the given string"""
    return value.replace(arg, "")

И вот пример использования этого фильтра:

{{ somevariable|cut:"0" }}

Большинство фильтров не принимают аргументы. В этом случае оставьте аргумент из вашей функции:

def lower(value):  # Only one argument.
    """Converts a string into all lowercase"""
    return value.lower()

Регистрация пользовательских фильтров

django.template.Library.filter()

После написания определения фильтра вам необходимо зарегистрировать его в вашем экземпляре Library, чтобы сделать его доступным для языка шаблонов Django:

register.filter("cut", cut)
register.filter("lower", lower)

Метод Library.filter() принимает два аргумента:

  1. Имя фильтра — строка.
  2. Функция компиляции — функция Python (а не имя функции как строка).

Вы можете использовать register.filter() в качестве декоратора вместо этого:

@register.filter(name="cut")
def cut(value, arg):
    return value.replace(arg, "")


@register.filter
def lower(value):
    return value.lower()

Если вы опустите аргумент name, как во втором примере выше, Django будет использовать имя функции в качестве имени фильтра.

Наконец, register.filter() также принимает три именованных аргумента: is_safe, needs_autoescape, и expects_localtime. Эти аргументы описаны в разделах фильтры и автоматическая экранизация и фильтры и часовые пояса ниже.

Фильтры шаблонов, которые ожидают строки

django.template.defaultfilters.stringfilter()

Если вы пишете фильтр шаблона, который ожидает только строку в качестве первого аргумента, вы должны использовать декоратор stringfilter. Это преобразует объект в его строковое значение перед передачей в вашу функцию:

from django import template
from django.template.defaultfilters import stringfilter

register = template.Library()


@register.filter
@stringfilter
def lower(value):
    return value.lower()

Таким образом, вы сможете передавать, например, целое число в этот фильтр, и это не вызовет AttributeError, (потому что у целых чисел нет методов lower()).

Фильтры и автоматическая экранизация

При написании пользовательского фильтра подумайте о том, как фильтр будет взаимодействовать с поведением автоматической экранизации Django. Обратите внимание, что внутри кода шаблона могут циркулировать два типа строк:

  • Необработанные строки — это обычные строки Python. При выводе они экранируются, если автоматическая экранизация активна, и выводятся без изменений в противном случае.
  • Безопасные строки — это строки, которые были помечены как безопасные от дальнейшей экранизации в момент вывода. Любая необходимая экранизация уже выполнена. Они часто используются для вывода, содержащего необработанный HTML, который должен интерпретироваться на стороне клиента как есть.

    Внутренне эти строки являются объектами типа SafeString. Вы можете проверить их с помощью кода типа:

    from django.utils.safestring import SafeString
    
    if isinstance(value, SafeString):
        # Do something with the "safe" string.
        ...
    

Код фильтра шаблона попадает в одну из двух ситуаций:

  1. Ваш фильтр не вводит никаких небезопасных для HTML символов (<, >, ', " или &) в результат, которые не были уже присутствуют. В этом случае вы можете позволить Django позаботиться о всех обработках автоматического экранирования за вас. Всё, что вам нужно сделать, это установить флаг is_safe в True при регистрации вашей функции фильтра, как показано ниже:

    @register.filter(is_safe=True)
    def myfilter(value):
        return value
    

    Этот флаг сообщает Django, что если в ваш фильтр передаётся «безопасная» строка, то результат по-прежнему будет «безопасным», а если передаётся небезопасная строка, Django автоматически экранирует её, если это необходимо.

    Вы можете рассматривать это как означающее «этот фильтр безопасен — он не вводит никаких потенциальных небезопасных HTML-символов».

    Причина, по которой is_safe необходима, заключается в том, что существует множество обычных операций со строками, которые преобразуют объект SafeData обратно в обычный объект str, и, вместо того чтобы пытаться их все перехватить, что было бы очень сложно, Django исправит повреждение после завершения фильтра.

    Например, предположим, что у вас есть фильтр, который добавляет строку xx в конец любого ввода. Поскольку это не вводит опасных символов HTML в результат (кроме тех, которые уже присутствовали), вы должны пометить свой фильтр флагом is_safe:

    @register.filter(is_safe=True)
    def add_xx(value):
        return "%sxx" % value
    

    Когда этот фильтр используется в шаблоне, где включено автоматическое экранирование, Django будет экранировать вывод всякий раз, когда ввод не помечен как «безопасный».

    По умолчанию, is_safe имеет значение False, и вы можете опустить его из любых фильтров, где он не требуется.

    Будьте осторожны при решении, действительно ли ваш фильтр оставляет безопасные строки безопасными. Если вы удаляете символы, вы можете непреднамеренно оставить несбалансированные HTML-теги или сущности в результате. Например, удаление > из ввода может превратить <a> в <a, которое необходимо экранировать при выводе, чтобы избежать проблем. Аналогично, удаление точки с запятой (;) может превратить &amp; в &amp, что больше не является допустимой сущностью и, следовательно, требует дальнейшего экранирования. Большинство случаев будут не такими сложными, но следите за возможными проблемами такого рода при проверке вашего кода.

    Пометка фильтра is_safe принудительно преобразует возвращаемое значение фильтра в строку. Если ваш фильтр должен возвращать булево значение или другое значение, не являющееся строкой, пометка его флагом is_safe может привести к нежелательным последствиям (например, преобразование булева False в строку ‘False’).

  2. В качестве альтернативы, код вашего фильтра может вручную обрабатывать необходимое экранирование. Это необходимо, когда вы вводите новые HTML-теги в результат. Вы хотите пометить вывод как безопасный от дальнейшего экранирования, чтобы ваши HTML-теги не были экранированы повторно, поэтому вам необходимо самостоятельно обрабатывать вход.

    Чтобы пометить вывод как безопасную строку, используйте django.utils.safestring.mark_safe().

    Однако будьте осторожны. Вам нужно сделать больше, чем просто пометить вывод как безопасный. Вам нужно убедиться, что он действительно «безопасен», и то, что вы делаете, зависит от того, включено ли автоматическое экранирование. Идея состоит в том, чтобы писать фильтры, которые могут работать в шаблонах, где автоматическое экранирование включено или выключено, чтобы облегчить работу авторам шаблонов.

    Для того чтобы ваш фильтр знал текущее состояние автоматического экранирования, установите флаг needs_autoescape в True при регистрации вашей функции фильтра. (Если вы не укажете этот флаг, он по умолчанию равен False). Этот флаг сообщает Django, что ваша функция фильтра хочет получить дополнительный ключевой аргумент, называемый autoescape, который равен True если автоматическое экранирование включено, и False в противном случае. Рекомендуется установить значение параметра autoescape по умолчанию в True, чтобы при вызове функции из Python кода экранирование было включено по умолчанию.

    Например, давайте напишем фильтр, который выделяет первый символ строки:

    from django import template
    from django.utils.html import conditional_escape
    from django.utils.safestring import mark_safe
    
    register = template.Library()
    
    
    @register.filter(needs_autoescape=True)
    def initial_letter_filter(text, autoescape=True):
        first, other = text[0], text[1:]
        if autoescape:
            esc = conditional_escape
        else:
            esc = lambda x: x
        result = "<strong>%s</strong>%s" % (esc(first), esc(other))
        return mark_safe(result)
    

    Флаги needs_autoescape и ключевой аргумент autoescape означают, что наша функция будет знать, включено ли автоматическое экранирование при вызове фильтра. Мы используем autoescape для определения, необходимо ли передавать входные данные через django.utils.html.conditional_escape или нет. (В последнем случае мы используем функцию тождества в качестве функции «экранирования».) Функция conditional_escape() похожа на escape(), за исключением того, что она экранирует только ввод, который не является экземпляром SafeData. Если в conditional_escape() передаётся экземпляр SafeData, данные возвращаются без изменений.

    Наконец, в приведенном выше примере мы помним, чтобы пометить результат как безопасный, чтобы наш 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 и имеет метод django.template.Node . Скомпилированный шаблон — это список объектов Node . Когда вы вызываете render() на объекте скомпилированного шаблона, шаблон вызывает метод render() на каждом Node в списке узлов с заданным контекстом. Результаты конкатенируются вместе, чтобы сформировать вывод шаблона.

Таким образом, для определения пользовательского тега шаблона вы указываете, как исходный тег шаблона преобразуется в Node (функция компиляции), и что делает метод render() узла.

Написание функции компиляции

Для каждого тега шаблона, с которым сталкивается анализатор шаблона, он вызывает функцию Python с содержимым тега и самим объектом анализатора. Эта функция отвечает за возврат экземпляра Node на основе содержимого тега.

Например, давайте напишем полную реализацию нашего тега шаблона, {% current_time %}, который отображает текущую дату/время, отформатированную в соответствии с параметром, заданным в теге, в синтаксисе strftime(). Перед всем остальным неплохо решить синтаксис тега. В нашем случае давайте скажем, что тег должен использоваться так:

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

Парсер для этой функции должен получить параметр и создать объект Node:

from django import template


def do_current_time(parser, token):
    try:
        # split_contents() knows not to split quoted strings.
        tag_name, format_string = token.split_contents()
    except ValueError:
        raise template.TemplateSyntaxError(
            "%r tag requires a single argument" % token.contents.split()[0]
        )
    if not (format_string[0] == format_string[-1] and format_string[0] in ('"', "'")):
        raise template.TemplateSyntaxError(
            "%r tag's argument should be in quotes" % tag_name
        )
    return CurrentTimeNode(format_string[1:-1])

Примечания:

  • parser — это объект парсера шаблона. Нам он не нужен в этом примере.
  • token.contents — это строка из исходного содержимого тега. В нашем примере это 'current_time "%Y-%m-%d %I:%M %p"'.
  • Метод token.split_contents() разделяет аргументы по пробелам, сохраняя вместе строки в кавычках. Более простой метод token.contents.split() не был бы таким надёжным, так как он бы наивно разбивал по всем пробелам, включая те, что находятся внутри кавычек. Лучше всегда использовать token.split_contents().
  • Эта функция отвечает за поднятие django.template.TemplateSyntaxError, с полезными сообщениями, для любой синтаксической ошибки.
  • Исключения TemplateSyntaxError используют переменную tag_name. Не зашифровывайте в коде имя тега в сообщениях об ошибках, так как это связанет имя тега с вашей функцией. token.contents.split()[0] всегда будет именем вашего тега, даже когда у тега нет аргументов.
  • Функция возвращает объект CurrentTimeNode со всем, что нужно узлу знать об этом теге. В данном случае он передаёт аргумент — "%Y-%m-%d %I:%M %p" . Ведущие и заключительные кавычки из тега шаблона удаляются в format_string[1:-1].
  • Анализ очень низкого уровня. Разработчики Django экспериментировали с написанием небольших фреймворков на основе этой системы анализа, используя такие методы, как грамматики EBNF, но эти эксперименты сделали движок шаблонов слишком медленным. Он низкоуровневый, потому что это быстрее всего.

Написание рендерера

Второй шаг в написании пользовательских тегов — определение подкласса Node, у которого есть метод render().

Продолжая предыдущий пример, нам нужно определить CurrentTimeNode:

import datetime
from django import template


class CurrentTimeNode(template.Node):
    def __init__(self, format_string):
        self.format_string = format_string

    def render(self, context):
        return datetime.datetime.now().strftime(self.format_string)

Примечания:

  • __init__() получает format_string из do_current_time() . Всегда передавайте любые опции/параметры/аргументы в Node через его метод __init__().
  • Метод render() — это место, где фактически происходит работа.
  • render() в целом должен работать молча, особенно в рабочей среде. Однако в некоторых случаях, особенно если context.template.engine.debug является True, этот метод может вызвать исключение для облегчения отладки. Например, несколько основных тегов поднимают django.template.TemplateSyntaxError, если они получают неправильное количество или тип аргументов.

В конечном итоге, это разделение компиляции и рендеринга приводит к эффективной системе шаблонов, так как шаблон может отображать несколько контекстов, не проходя повторный анализ.

Учитывая авто-экранирование

Вывод тегов шаблонов не автоматически проходит через фильтры авто-экранирования (за исключением simple_tag(), как описано выше). Однако при написании тега шаблона есть несколько моментов, которые следует учитывать.

Если метод render() вашего тега шаблона сохраняет результат в переменной контекста (а не возвращает результат в строке), он должен позаботиться о вызове mark_safe() при необходимости. Когда переменная в конечном итоге отображается, она будет затронута настройкой авто-экранирования, действующей в этот момент, поэтому содержимое, которое должно быть защищено от дальнейшего экранирования, должно быть отмечено как таковое.

Также, если ваш тег шаблона создаёт новый контекст для выполнения некоторого дочернего рендеринга, установите атрибут auto-escape в текущее значение контекста. Метод __init__ для класса Context принимает параметр, который называется autoescape, который вы можете использовать для этой цели. Например:

from django.template import Context


def render(self, context):
    # ...
    new_context = Context({"var": obj}, autoescape=context.autoescape)
    # ... Do something with new_context ...

Это не очень распространённая ситуация, но она полезна, если вы сами отображаете шаблон. Например:

def render(self, context):
    t = context.template.engine.get_template("small_fragment.html")
    return t.render(Context({"var": obj}, autoescape=context.autoescape))

Если бы мы пренебрегли передачей текущего значения context.autoescape в наш новый Context в этом примере, результаты всегда были бы автоматически экранированы, что может не быть желаемым поведением, если тег шаблона используется внутри блока {% autoescape off %}.

Учитывая безопасность потоков тегов шаблонов

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

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

Spec-Zone.ru

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