Настраиваемые теги и фильтры шаблонов
Структура кода
Наиболее распространённое место для определения настраиваемых тегов и фильтров шаблонов — внутри приложения 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. При выводе они экранируются, если включена автоматическая экранизация, и отображаются без изменений, в противном случае.
-
Безопасные строки — это строки, которые были помечены как безопасные от дальнейшей экранизации во время вывода. Любая необходимая экранизация уже выполнена. Они часто используются для вывода, содержащего необработанный HTML, который должен интерпретироваться как есть на стороне клиента.
Внутри эти строки имеют тип
SafeText. Вы можете проверить их с помощью кода, подобного этому:from django.utils.safestring import SafeText if isinstance(value, SafeText): # Do something with the "safe" string. ...
Код фильтра шаблонов попадает в одну из двух ситуаций:
-
Ваш фильтр не вводит никаких небезопасных для 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, что потребует экранирования при выводе, чтобы избежать проблем. Аналогично, удаление точки с запятой (;) может превратить&в&, что больше не является допустимой сущностью и, следовательно, требует дальнейшего экранирования. Большинство случаев будут не такими сложными, но следите за подобными проблемами при проверке кода.Пометка фильтра
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().
Если вашему тегу шаблона нужно получить доступ к текущему контексту, вы можете использовать аргумент 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.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(). - Метод
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 выполняет свою первую итерацию цикла,
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, должна храниться в render_context.
Примечание
Обратите внимание, как мы использовали 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/2.1/howto/custom-template-tags/