Как создать пользовательские теги и фильтры шаблонов
Структура кода
Самое распространенное место для определения пользовательских тегов и фильтров шаблонов — внутри приложения 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, который должен интерпретироваться на стороне клиента как есть.
Внутренне эти строки являются типа
SafeString. Вы можете проверить их, используя код, подобный этому:from django.utils.safestring import SafeString if isinstance(value, SafeString): # 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 ''
Когда этот флаг установлен, если первый аргумент вашего фильтра является временем с учетом часового пояса, 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(). Всегда передавайте все параметры/аргументы вNodeчерез его__init__(). - Метод
render()— это то место, где фактически происходит работа. -
render()обычно должен завершаться без ошибок, особенно в рабочей среде. Однако в некоторых случаях, особенно еслиcontext.template.engine.debugявляетсяTrue, этот метод может вызвать исключение, чтобы упростить отладку. Например, несколько основных тегов вызываютdjango.template.TemplateSyntaxErrorесли они получают неверное количество или тип аргументов.
В конечном итоге, это разделение компиляции и рендеринга приводит к эффективной системе шаблонов, поскольку шаблон может рендерить несколько контекстов без необходимости многократной обработки.
Учёт автоматического экранирования
Вывод из тегов шаблонов не автоматически обрабатывается фильтрами автоматического экранирования (за исключением simple_tag(), как описано выше). Однако при написании тега шаблона следует учитывать несколько моментов.
Если метод render() вашего тега шаблона сохраняет результат в переменной контекста (а не возвращает результат в строке), он должен позаботиться о вызове mark_safe() при необходимости. Когда переменная в конечном итоге будет рендериться, на неё повлияет установленное значение автоматического экранирования в момент времени, поэтому содержимое, которое должно быть защищено от дальнейшего экранирования, должно быть помечено как таковое.
Кроме того, если ваш тег шаблона создаёт новый контекст для выполнения некоторого подрендеринга, установите атрибут автоматического экранирования в текущее значение контекста. Метод __init__ для класса Context принимает параметр autoescape, который вы можете использовать для этой цели. Например:
from django.template import Context
def render(self, context):
# ...
new_context = Context({'var': obj}, autoescape=context.autoescape)
# ... Do something with new_context ...
Это не очень распространённая ситуация, но она полезна, если вы сами рендерите шаблон. Например:
def render(self, context):
t = context.template.engine.get_template('small_fragment.html')
return t.render(Context({'var': obj}, autoescape=context.autoescape))
Если мы пренебрегли передачей текущего значения context.autoescape в наш новый Context в этом примере, результаты всегда были бы автоматически экранированы, что может не быть желаемым поведением, если тег шаблона используется внутри блока {% autoescape off %}.
Учёт многопоточности тегов шаблонов
После того, как узел был обработан, его метод render может быть вызван любое количество раз. Поскольку Django иногда запускается в многопоточных средах, один узел может одновременно рендериться с разными контекстами в ответ на два отдельных запроса. Поэтому важно убедиться, что ваши теги шаблонов потокобезопасны.
Чтобы убедиться, что ваши теги шаблона безопасны в многопоточной среде, вы никогда не должны хранить информацию о состоянии в самом узле. Например, Django предоставляет встроенный тег шаблона cycle, который циклически перебирает список заданных строк каждый раз при рендеринге:
{% for o in some_list %}
<tr class="{% cycle 'row1' 'row2' %}">
...
</tr>
{% endfor %}
Примитивное реализация CycleNode может выглядеть примерно так:
import itertools
from django import template
class CycleNode(template.Node):
def __init__(self, cyclevars):
self.cycle_iter = itertools.cycle(cyclevars)
def render(self, context):
return next(self.cycle_iter)
Но предположим, что у нас есть два шаблона, которые одновременно отображают фрагмент шаблона из примера выше:
- Поток 1 выполняет свою первую итерацию цикла,
CycleNode.render()возвращает ‘row1’ - Поток 2 выполняет свою первую итерацию цикла,
CycleNode.render()возвращает ‘row2’ - Поток 1 выполняет свою вторую итерацию цикла,
CycleNode.render()возвращает ‘row1’ - Поток 2 выполняет свою вторую итерацию цикла,
CycleNode.render()возвращает ‘row2’
Узел цикла выполняет итерацию, но выполняет ее глобально. Для потоков 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/4.1/howto/custom-template-tags/