Spec-Zone.ru › Django 6.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. Если в 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 ""

Если этот флаг установлен и первый аргумент фильтра — объект даты и времени с часовым поясом, 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 5.2.
django.template.Library.simple_block_tag()

Если нужно передать пользовательскому тегу отрисованный фрагмент шаблона, Django предоставляет вспомогательную функцию simple_block_tag. Подобно simple_tag(), эта функция принимает пользовательскую функцию тега, но дополнительно использует аргумент content, содержащий отрисованное содержимое, определённое внутри тега. Благодаря этому динамические фрагменты шаблона можно легко включать в пользовательские теги.

Например, пользовательский блочный тег для создания диаграммы может выглядеть так:

from django import template
from myapp.charts import render_chart

register = template.Library()


@register.simple_block_tag
def chart(content):
    return render_chart(source=content)

Аргумент content содержит всё, что находится между тегами {% chart %} и {% endchart %}:

{% chart %}
  digraph G {
      label = "Chart for {{ request.user }}"
      A -> {B C}
  }
{% endchart %}

Если внутри блока content находятся другие теги или переменные шаблона, они будут обработаны до передачи функции тега. В приведённом выше примере значение request.user будет определено к моменту вызова render_chart.

Блочные теги закрываются с помощью end{name} (например, endchart). Это можно изменить с помощью параметра end_name:

@register.simple_block_tag(end_name="endofchart")
def chart(content):
    return render_chart(source=content)

Для этого потребуется следующее определение в шаблоне:

{% chart %}
  digraph G {
      label = "Chart for {{ request.user }}"
      A -> {B C}
  }
{% endofchart %}

Обратите внимание на несколько особенностей simple_block_tag:

  • Первый аргумент должен называться content; он будет содержать содержимое тега шаблона в виде отрисованной строки.
  • Переменные, переданные тегу, не включаются в контекст отрисовки содержимого, как это происходит при использовании тега {% with %}.

Как и simple_tag, simple_block_tag:

  • Проверяет количество и корректность аргументов.
  • При необходимости удаляет кавычки из аргументов.
  • Соответствующим образом экранирует результат.
  • Поддерживает передачу takes_context=True при регистрации для доступа к контексту. Обратите внимание: в этом случае первый аргумент пользовательской функции должен называться context, а за ним должен следовать content.
  • Поддерживает переименование тега с помощью аргумента name при регистрации.
  • Поддерживает произвольное количество позиционных и именованных аргументов.
  • Поддерживает сохранение результата в переменную шаблона с помощью варианта as.

Экранирование содержимого

В отношении автоэкранирования simple_block_tag ведёт себя аналогично simple_tag. Подробнее об экранировании и безопасности см. в simple_tag. Поскольку аргумент content уже обработан Django, он уже экранирован.

Полный пример

Рассмотрим пользовательский тег шаблона, который создаёт блок сообщения с поддержкой нескольких уровней важности и содержимого, выходящего за рамки простой фразы. Его можно реализовать с помощью simple_block_tag следующим образом:

testapp/templatetags/testapptags.py
from django import template
from django.utils.html import format_html


register = template.Library()


@register.simple_block_tag(takes_context=True)
def msgbox(context, content, level):
    format_kwargs = {
        "level": level.lower(),
        "level_title": level.capitalize(),
        "content": content,
        "open": " open" if level.lower() == "error" else "",
        "site": context.get("site", "My Site"),
    }
    result = """
    <div class="msgbox {level}">
      <details{open}>
        <summary>
          <strong>{level_title}</strong>: Please read for <i>{site}</i>
        </summary>
        <p>
          {content}
        </p>
      </details>
    </div>
    """
    return format_html(result, **format_kwargs)

В сочетании с минимальным представлением и соответствующим шаблоном, как показано ниже:

testapp/views.py
from django.shortcuts import render


def simpleblocktag_view(request):
    return render(request, "test.html", context={"site": "Important Site"})
testapp/templates/test.html
{% extends "base.html" %}

{% load testapptags %}

{% block content %}

  {% msgbox level="error" %}
    Please fix all errors. Further documentation can be found at
    <a href="http://example.com">Docs</a>.
  {% endmsgbox %}

  {% msgbox level="info" %}
    More information at: <a href="http://othersite.com">Other Site</a>/
  {% endmsgbox %}

{% endblock %}

В результате отрисовки будет получен следующий HTML:

<div class="msgbox error">
  <details open>
    <summary>
      <strong>Error</strong>: Please read for <i>Important Site</i>
    </summary>
    <p>
      Please fix all errors. Further documentation can be found at
      <a href="http://example.com">Docs</a>.
    </p>
  </details>
</div>

<div class="msgbox info">
  <details>
    <summary>
      <strong>Info</strong>: Please read for <i>Important Site</i>
    </summary>
    <p>
      More information at: <a href="http://othersite.com">Other Site</a>
    </p>
  </details>
</div>

Включающие теги

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/6.0/howto/custom-template-tags/

Spec-Zone.ru

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