Пользовательские теги и фильтры шаблонов
Язык шаблонов 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() принимает два аргумента:
- Имя фильтра — строка.
- Функция компиляции — функция 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
strилиunicode. При выводе они экранируются, если автоматическое экранирование включено, и выводятся без изменений, иначе. -
Безопасные строки — это строки, которые были помечены как безопасные от дальнейшего экранирования во время вывода. Любое необходимое экранирование уже выполнено. Они обычно используются для вывода, содержащего необработанный HTML, который предполагается интерпретировать как есть на стороне клиента.
Внутренне эти строки имеют тип
SafeBytesилиSafeText. Они используют общий базовый классSafeData, поэтому вы можете проверить их с помощью кода, такого как:if isinstance(value, SafeData): # Do something with the "safe" string. ... -
Строки, помеченные как «требующие экранирования», всегда экранируются при выводе, независимо от того, находятся ли они в блоке
autoescapeили нет. Однако эти строки экранируются только один раз, даже если автоматическое экранирование применяется.Внутренне эти строки имеют тип
EscapeBytesилиEscapeText. Обычно об этом не нужно беспокоиться; они существуют для реализации фильтраescape.
Код фильтра шаблона попадает в одну из двух ситуаций:
-
Ваш фильтр не вводит в результат никакие небезопасные для HTML-символы (
<,>,',"или&) которые не были уже присутствовать. В этом случае вы можете позволить Django позаботиться о всей автоматической обработке экранирования за вас. Всё что вам нужно сделать, это установить флагis_safeв значениеTrueпри регистрации функции-фильтра, как показано ниже:@register.filter(is_safe=True) def myfilter(value): return valueЭтот флаг сообщает Django, что если в ваш фильтр передаётся "безопасная" строка, результат всё равно будет "безопасным", а если передаётся небезопасная строка, Django автоматически экранирует её, если необходимо.
Вы можете рассматривать это как "данный фильтр безопасен – он не вносит никакой возможности появления небезопасного HTML".
Причина, по которой
is_safeнеобходима, заключается в том, что есть много обычных строковых операций, которые превратят объектSafeDataобратно в обычный объектstrилиunicode, и вместо того, чтобы пытаться перехватить их все, что было бы очень сложно, Django исправит повреждение после завершения работы фильтра.Например, предположим, что у вас есть фильтр, который добавляет строку
xxв конец любого входного значения. Поскольку это не добавляет никаких опасных HTML-символов в результат (кроме тех, которые уже присутствовали), вы должны пометить свой фильтр какis_safe:@register.filter(is_safe=True) def add_xx(value): return '%sxx' % valueКогда этот фильтр используется в шаблоне, где включено автоматическое экранирование, Django будет экранировать вывод всякий раз, когда входное значение не помечено как "безопасное".
По умолчанию,
is_safeравноFalse, и вы можете опустить его из любых фильтров, где он не требуется.Будьте осторожны при определении, действительно ли ваш фильтр оставляет безопасные строки безопасными. Если вы удаляете символы, вы можете непреднамеренно оставить несбалансированные HTML-теги или сущности в результате. Например, удаление
>из входных данных может превратить<a>в<a, что необходимо экранировать при выводе, чтобы избежать проблем. Аналогично, удаление точки с запятой (;) может превратить&в&, что больше не является допустимой сущностью и, следовательно, требует дальнейшего экранирования. Большинство случаев будут не такими сложными, но следите за подобными проблемами при просмотре вашего кода.Пометка фильтра
is_safeприведет к принудительному преобразованию возвращаемого значения фильтра в строку. Если ваш фильтр должен возвращать булево значение или другое значение, не являющееся строкой, пометка его какis_safeвероятно, приведет к непреднамеренным последствиям (например, преобразованию булевого значения False в строку 'False'). -
В качестве альтернативы, код вашего фильтра может вручную обрабатывать необходимое экранирование. Это необходимо, когда вы вводите новые 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.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() настоятельно рекомендуется.
Автоматическое экранирование для simple_tag (как описано в предыдущих двух абзацах) было добавлено.
Если вашему тегу шаблона необходимо получить доступ к текущему контексту, вы можете использовать аргумент 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.template.Library.assignment_tag()
Устаревшее с версии 1.9: simple_tag теперь может хранить результаты в переменной шаблона и должно использоваться вместо него.
Для упрощения создания тегов, устанавливающих переменную в контексте, Django предоставляет вспомогательную функцию assignment_tag. Эта функция работает так же, как simple_tag(), за исключением того, что она сохраняет результат тега в указанной переменной контекста вместо непосредственного вывода.
Наша предыдущая функция current_time может быть переписана следующим образом:
@register.assignment_tag
def get_current_time(format_string):
return datetime.datetime.now().strftime(format_string)
Вы можете сохранить результат в переменной шаблона, используя аргумент as и за ним имя переменной, и вывести его по своему усмотрению:
{% get_current_time "%Y-%m-%d %I:%M %p" as the_time %}
<p>The time is {{ the_time }}.</p>
Расширенные пользовательские теги шаблонов
Иногда базовые возможности создания пользовательских тегов шаблона недостаточны. Не волнуйтесь, 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))
Атрибут template объектов Context был добавлен в Django 1.8. Следует использовать context.template.engine.get_template вместо django.template.loader.get_template(), так как последний теперь возвращает обёртку, метод render которой не принимает Context.
Если бы мы забыли передать значение текущего 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 выполняет свою первую итерацию цикла,
CycleNode.render()возвращает ‘row1’ - Поток 2 выполняет свою первую итерацию цикла,
CycleNode.render()возвращает ‘row2’ - Поток 1 выполняет свою вторую итерацию цикла,
CycleNode.render()возвращает ‘row1’ - Поток 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, следует хранить в CycleNode.
Примечание
Обратите внимание, как мы использовали self для ограничения информации, специфичной для CycleNode, в рамках render_context. В данном шаблоне может быть несколько CycleNodes, поэтому нужно быть осторожным, чтобы не перезаписывать информацию о состоянии других узлов. Самый простой способ сделать это — всегда использовать self в качестве ключа для render_context. Если вы отслеживаете несколько переменных состояния, сделайте render_context[self] словарем.
Регистрация тега
Наконец, зарегистрируйте тег в экземпляре Library вашего модуля, как описано в создании пользовательских фильтров шаблонов выше. Пример:
register.tag('current_time', do_current_time)
Метод tag() принимает два аргумента:
- Имя тега шаблона — строка. Если этот аргумент опущен, будет использовано имя функции компиляции.
- Функция компиляции — функция 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() вернёт три значения:
- Имя тега
format_time. - Строка
'blog_entry.date_updated'(без окружающих кавычек). - Строка форматирования
'"%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/1.9/howto/custom-template-tags/