Spec-Zone.ru › Django 2.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, который должен интерпретироваться как есть на стороне клиента.

    Внутри эти строки имеют тип SafeText. Вы можете проверить их, используя код, подобный этому:

    from django.utils.safestring import SafeText
    
    if isinstance(value, SafeText):
        # 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() при необходимости. Когда переменная в конечном итоге будет отображаться, она будет подвергнута влиянию настройки автоэкранирования, действующей в этот момент, поэтому контент, который должен быть защищен от дальнейшего экранирования, должен быть помечен как такой.

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

from django.template import Context

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

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

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

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

Безопасность потоков

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

Чтобы убедиться, что ваши теги шаблонов безопасны для потоков, не следует хранить информацию о состоянии в самом узле. Например, Django предоставляет встроенный тег шаблона cycle, который циклически проходит по списку заданных строк каждый раз, когда он отображается:

{% for o in some_list %}
    <tr class="{% cycle 'row1' 'row2' %}">
        ...
    </tr>
{% endfor %}

Примитивная реализация CycleNode может выглядеть так:

import itertools
from django import template

class CycleNode(template.Node):
    def __init__(self, cyclevars):
        self.cycle_iter = itertools.cycle(cyclevars)

    def render(self, context):
        return next(self.cycle_iter)

Но предположим, что у нас есть два шаблона, отображающие фрагмент шаблона сверху одновременно:

  1. Поток 1 выполняет свою первую итерацию цикла, CycleNode.render() возвращает ‘row1’
  2. Поток 2 выполняет свою первую итерацию цикла, CycleNode.render() возвращает ‘row2’
  3. Поток 1 выполняет свою вторую итерацию цикла, CycleNode.render() возвращает ‘row1’
  4. Поток 2 выполняет свою вторую итерацию цикла, CycleNode.render() возвращает ‘row2’

Узел CycleNode итерируется, но он итерируется глобально. Что касается потока 1 и потока 2, он всегда возвращает то же значение. Это, очевидно, не то, что мы хотим!

Для решения этой проблемы Django предоставляет render_context, связанный с context текущей шаблона, который в данный момент отображается. render_context ведет себя как словарь Python и должен использоваться для хранения Node состояния между вызовами метода render.

Давайте переработаем нашу реализацию CycleNode с использованием render_context.

class CycleNode(template.Node):
    def __init__(self, cyclevars):
        self.cyclevars = cyclevars

    def render(self, context):
        if self not in context.render_context:
            context.render_context[self] = itertools.cycle(self.cyclevars)
        cycle_iter = context.render_context[self]
        return next(cycle_iter)

Обратите внимание, что совершенно безопасно хранить глобальную информацию, которая не будет изменяться на протяжении всего жизненного цикла Node, в качестве атрибута. В случае CycleNode, аргумент cyclevars не изменяется после того, как Node был создан, поэтому нам не нужно помещать его в render_context. Но информация о состоянии, специфичная для текущего шаблона, который отображается, например, текущая итерация CycleNode, должна храниться в render_context.

Примечание

Обратите внимание, как мы использовали self для ограничения информации, специфичной для CycleNode, внутри render_context. В данном шаблоне может быть несколько CycleNodes, поэтому нам нужно быть осторожными, чтобы не перезаписывать информацию о состоянии другого узла. Самый простой способ сделать это – всегда использовать self в качестве ключа в render_context. Если вы отслеживаете несколько переменных состояния, сделайте render_context[self] словарем.

Регистрация тега

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

register.tag('current_time', do_current_time)

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

  1. Имя тега шаблона – строка. Если это опущено, будет использовано имя функции компиляции.
  2. Функция компиляции – функция Python (а не имя функции как строка).

Как и при регистрации фильтров, это также можно использовать как декоратор:

@register.tag(name="current_time")
def do_current_time(parser, token):
    ...

@register.tag
def shout(parser, token):
    ...

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

Передача переменных шаблона тегу

Хотя вы можете передать любое количество аргументов тегу шаблона, используя token.split_contents(), все аргументы передаются как строковые литералы. Для передачи динамического содержимого (переменной шаблона) тегу шаблона в качестве аргумента требуется немного больше работы.

В то время как в предыдущих примерах текущее время форматировалось в строку и возвращалась эта строка, предположим, что вы хотите передать DateTimeField из объекта и иметь тег шаблона форматировать эту дату и время:

<p>This post was last updated at {% format_time blog_entry.date_updated "%Y-%m-%d %I:%M %p" %}.</p>

Изначально token.split_contents() вернёт три значения:

  1. Имя тега format_time.
  2. Строка 'blog_entry.date_updated' (без окружающих кавычек).
  3. Строка форматирования '"%Y-%m-%d %I:%M %p"'. Значение, возвращаемое из split_contents(), будет включать ведущие и хвостовые кавычки для строковых литералов такого вида.

Теперь ваш тег должен выглядеть примерно так:

from django import template

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

Также вам нужно изменить обработчик, чтобы получить фактическое содержимое свойства date_updated объекта blog_entry. Это можно сделать, используя класс Variable() в django.template.

Для использования класса Variable, просто создайте его с именем разрешаемой переменной и затем вызовите variable.resolve(context). Например:

class FormatTimeNode(template.Node):
    def __init__(self, date_to_be_formatted, format_string):
        self.date_to_be_formatted = template.Variable(date_to_be_formatted)
        self.format_string = format_string

    def render(self, context):
        try:
            actual_date = self.date_to_be_formatted.resolve(context)
            return actual_date.strftime(self.format_string)
        except template.VariableDoesNotExist:
            return ''

Разрешение переменных вызовет исключение VariableDoesNotExist, если оно не сможет разрешить переданную в него строку в текущем контексте страницы.

Установка переменной в контексте

Приведённые выше примеры просто выводят значение. Обычно более гибко, если ваши теги шаблонов устанавливают переменные шаблона, а не выводят значения. Таким образом, авторы шаблонов могут повторно использовать значения, созданные вашими тегами шаблонов.

Для установки переменной в контексте просто используйте присвоение словаря к объекту контекста в методе render() Вот обновлённая версия CurrentTimeNode, которая устанавливает переменную шаблона current_time, а не выводит её:

import datetime
from django import template

class CurrentTimeNode2(template.Node):
    def __init__(self, format_string):
        self.format_string = format_string
    def render(self, context):
        context['current_time'] = datetime.datetime.now().strftime(self.format_string)
        return ''

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

Вот как вы бы использовали эту новую версию тега:

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

Область действия переменных в контексте

Любая переменная, установленная в контексте, будет доступна только в том же block шаблона, в котором она была назначена. Это поведение преднамеренное; оно обеспечивает область видимости переменных, чтобы они не конфликтовали с контекстом в других блоках.

Однако, существует проблема с CurrentTimeNode2: имя переменной current_time жёстко закодировано. Это означает, что вам нужно убедиться, что ваш шаблон не использует {{ current_time }} где-либо ещё, потому что {% current_time %} будет слепо перезаписывать значение этой переменной. Более чистое решение – заставить тег шаблона указать имя переменной вывода, как показано ниже:

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

Для этого вам нужно переработать как функцию компиляции, так и класс Node, как показано ниже:

import re

class CurrentTimeNode3(template.Node):
    def __init__(self, format_string, var_name):
        self.format_string = format_string
        self.var_name = var_name
    def render(self, context):
        context[self.var_name] = datetime.datetime.now().strftime(self.format_string)
        return ''

def do_current_time(parser, token):
    # This version uses a regular expression to parse tag contents.
    try:
        # Splitting by None == splitting by spaces.
        tag_name, arg = token.contents.split(None, 1)
    except ValueError:
        raise template.TemplateSyntaxError(
            "%r tag requires arguments" % token.contents.split()[0]
        )
    m = re.search(r'(.*?) as (\w+)', arg)
    if not m:
        raise template.TemplateSyntaxError("%r tag had invalid arguments" % tag_name)
    format_string, var_name = m.groups()
    if not (format_string[0] == format_string[-1] and format_string[0] in ('"', "'")):
        raise template.TemplateSyntaxError(
            "%r tag's argument should be in quotes" % tag_name
        )
    return CurrentTimeNode3(format_string[1:-1], var_name)

Разница здесь заключается в том, что do_current_time() получает строку форматирования и имя переменной, передавая оба значения CurrentTimeNode3.

Наконец, если вам нужен только простой синтаксис для вашего пользовательского тега шаблона, обновляющего контекст, рассмотрите использование сокращения simple_tag(), которое поддерживает присвоение результатов тега переменной шаблона.

Разбор до следующего тега блока

Теги шаблонов могут работать вместе. Например, стандартный тег {% comment %} скрывает всё до {% endcomment %}. Чтобы создать такой тег шаблона, используйте parser.parse() в своей функции компиляции.

Вот как может быть реализован упрощённый тег {% comment %}:

def do_comment(parser, token):
    nodelist = parser.parse(('endcomment',))
    parser.delete_first_token()
    return CommentNode()

class CommentNode(template.Node):
    def render(self, context):
        return ''

Примечание

Фактическая реализация {% comment %} немного отличается тем, что она позволяет отображаться недопустимым тегам шаблона между {% comment %} и {% endcomment %}. Это делается путем вызова parser.skip_past('endcomment'), а не parser.parse(('endcomment',)) за которым следует parser.delete_first_token(), тем самым избегая генерации списка узлов.

parser.parse() принимает кортеж имён тегов блоков «для разбора до»‘. Он возвращает экземпляр django.template.NodeList, который представляет собой список всех Node объектов, которые парсер встретил «до» того, как встретил любой из тегов, перечисленных в кортеже.

В "nodelist = parser.parse(('endcomment',))" в приведённом выше примере nodelist является списком всех узлов между {% comment %} и {% endcomment %}, не считая сами {% comment %} и {% endcomment %}.

После вызова parser.parse(), парсер ещё «не прочитал» тег {% endcomment %}, поэтому код должен явно вызвать parser.delete_first_token().

CommentNode.render() просто возвращает пустую строку. Всё, что находится между {% comment %} и {% endcomment %}, игнорируется.

Разбор до другого тега блока и сохранение содержимого

В предыдущем примере do_comment() отбрасывал всё, что находилось между {% comment %} и {% endcomment %}. Вместо этого можно что-то сделать с кодом между тегами блоков.

Например, вот пользовательский тег шаблона {% upper %}, преобразующий всё между собой и {% endupper %} в верхний регистр.

Использование:

{% upper %}This will appear in uppercase, {{ your_name }}.{% endupper %}

Как и в предыдущем примере, мы будем использовать parser.parse(). Но на этот раз мы передадим полученный nodelist в Node:

def do_upper(parser, token):
    nodelist = parser.parse(('endupper',))
    parser.delete_first_token()
    return UpperNode(nodelist)

class UpperNode(template.Node):
    def __init__(self, nodelist):
        self.nodelist = nodelist
    def render(self, context):
        output = self.nodelist.render(context)
        return output.upper()

Единственная новая концепция здесь – self.nodelist.render(context) в UpperNode.render().

Для получения более подробных примеров сложной отрисовки, см. исходный код {% for %} в django/template/defaulttags.py и {% if %} в django/template/smartif.py.

© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/2.0/howto/custom-template-tags/

Spec-Zone.ru

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