How to create custom template tags and filters

Язык шаблонов Django поставляется с широким набором built-in tags and filters, разработанным для удовлетворения потребностей вашего приложения в логике представления. Тем не менее, вам может понадобиться функциональность, которая не покрывается основным набором примитивов шаблонов. Вы можете расширить механизм шаблонов, определив пользовательские теги и фильтры с помощью 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. Это полезно, если вы хотите использовать при загрузке шаблонных тегов метку, отличную от имени модуля тегов шаблонов. Это также позволяет регистрировать теги без установки приложения.

За кулисами

For a ton of examples, read the source code for Django’s default filters and tags. They’re in django/template/defaultfilters.py and django/template/defaulttags.py, respectively.

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

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

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

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

Например, в фильтр {{ 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. Эти аргументы описаны ниже в filters and auto-escaping и filters and time zones.

Шаблонные фильтры, которые ожидают строк

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 или нет. (В последнем случае мы используем функцию identity в качестве функции «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 ''

Когда этот флаг установлен, если первым аргументом вашего фильтра является дататайм с учетом часового пояса, Django преобразует его в текущий часовой пояс, прежде чем передать его в ваш фильтр, когда это необходимо, в соответствии с rules for time zones conversions in templates.

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

Теги сложнее, чем фильтры, потому что теги могут делать все, что угодно. 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(), если контекст шаблона находится в режиме autoescape, чтобы обеспечить корректность 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, см. раздел inclusion tags.

Если вам нужно переименовать тег, вы можете задать для него пользовательское имя:

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, такого как был создан в tutorials. Мы будем использовать тег следующим образом:

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

Кроме того, если тег шаблона создает новый контекст для выполнения суб-рендеринга, установите атрибут 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 вашего модуля, как описано выше в writing custom template tags. Пример:

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() принимает кортеж имен блочных тегов „“to parse until““. Он возвращает экземпляр 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 more examples of complex rendering, see the source code of {% for %} in django/template/defaulttags.py and {% if %} in django/template/smartif.py.

Вернуться на верх