Пользовательские теги и фильтры шаблонов
Язык шаблонов Django поставляется с широким набором встроенных тегов и фильтров, предназначенных для удовлетворения потребностей вашей приложения в логике представления. Тем не менее, вам может потребоваться функциональность, которая не покрывается базовым набором примитивов шаблонов. Вы можете расширить движок шаблонов, определив пользовательские теги и фильтры с помощью Python, а затем сделать их доступными для ваших шаблонов, используя тег {% load %}.
Структура кода
Пользовательские теги и фильтры шаблонов должны храниться внутри приложения Django. Если они относятся к существующему приложению, имеет смысл объединить их там; в противном случае следует создать новое приложение для их хранения.
Приложение должно содержать директорию 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()
За кулисами
Для получения множества примеров прочитайте исходный код для стандартных фильтров и тегов 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:
- Проверка на требуемое количество аргументов и т. д. уже выполнена к тому времени, когда вызывается наша функция, поэтому нам не нужно этого делать.
- Кавычки вокруг аргумента (если таковые имеются) уже удалены, поэтому мы получаем просто строку.
- Если аргумент был переменной шаблона, наша функция получает текущее значение переменной, а не саму переменную.
Если вашему тегу шаблона необходимо получить доступ к текущему контексту, вы можете использовать аргумент 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 %}
Теги включения
-
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()
Для упрощения создания тегов, устанавливающих переменную в контексте, Django предоставляет вспомогательную функцию assignment_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>
Если вашему тегу шаблона требуется доступ к текущему контексту, вы можете использовать аргумент takes_context при регистрации своего тега:
@register.assignment_tag(takes_context=True)
def get_current_time(context, format_string):
timezone = context['timezone']
return your_get_current_time_method(timezone, format_string)
Обратите внимание, что первый параметр функции должен называться context.
Дополнительную информацию о работе опции takes_context см. в разделе о тегах включения.
Функции assignment_tag могут принимать любое количество позиционных или именованных аргументов. Например:
@register.assignment_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 the_result %}
Расширенные пользовательские теги шаблонов
Иногда базовых возможностей для создания пользовательских тегов шаблонов недостаточно. Не беспокойтесь, 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при получении неправильного количества или типа аргументов.
В конечном итоге, это разделение компиляции и рендеринга приводит к эффективной системе шаблонов, потому что шаблон может отобразить несколько контекстов без многократного анализа.
Учет автоэкранирования
Вывод из тегов шаблонов не автоматически проходит через фильтры автоэкранирования. Однако есть несколько моментов, которые следует учитывать при написании тега шаблона.
Если функция 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))
Атрибут template объектов Context был добавлен в Django 1.8. Вместо django.template.loader.get_template() следует использовать context.template.engine.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, должна храниться в 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.
Наконец, если вам нужен простой синтаксис для вашего пользовательского тега шаблона, обновляющего контекст, вы можете рассмотреть возможность использования сокращения тега присваивания, которое мы представили выше.
Разбор до следующего тега блока
Теги шаблонов могут работать совместно. Например, стандартный тег {% 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.8/howto/custom-template-tags/