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

Spec-Zone.ru

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