Настраиваемые теги и фильтры шаблонов
Макет кода
Наиболее распространенное место для определения настраиваемых тегов и фильтров шаблонов — внутри приложения Django. Если они относятся к существующему приложению, имеет смысл объединить их там; в противном случае их можно добавить в новое приложение. Когда приложение Django добавляется в INSTALLED_APPS, любые теги, определенные им в стандартном расположении, описанном ниже, автоматически становятся доступными для загрузки в шаблонах.
Приложение должно содержать каталог templatetags, на том же уровне, что и models.py, views.py, и т.д. Если его еще нет, создайте его — не забудьте файл __init__.py, чтобы убедиться, что каталог обрабатывается как пакет Python.
Сервер разработки не будет автоматически перезапускаться
После добавления модуля templatetags вам необходимо перезапустить сервер, прежде чем вы сможете использовать теги или фильтры в шаблонах.
Ваши настраиваемые теги и фильтры будут храниться в модуле внутри каталога templatetags. Имя файла модуля — это имя, которое вы будете использовать для загрузки тегов позже, поэтому будьте внимательны, чтобы оно не конфликтовало с настраиваемыми тегами и фильтрами в другом приложении.
Например, если ваши настраиваемые теги/фильтры находятся в файле poll_extras.py, структура вашего приложения может выглядеть так:
polls/
__init__.py
models.py
templatetags/
__init__.py
poll_extras.py
views.py
И в вашем шаблоне вы бы использовали следующее:
{% load poll_extras %}
Приложение, содержащее настраиваемые теги, должно быть в INSTALLED_APPS, чтобы тег {% load %} работал. Это функция безопасности: она позволяет размещать код Python для многих библиотек шаблонов на одном хост-компьютере, не предоставляя доступ ко всем из них для каждой установки Django.
Нет ограничений на количество модулей, которые вы можете поместить в пакет templatetags. Просто имейте в виду, что оператор {% load %} загрузит теги/фильтры для данного имени Python-модуля, а не имени приложения.
Чтобы быть действительной библиотекой тегов, модуль должен содержать переменную уровня модуля с именем register, которая является экземпляром template.Library, в котором зарегистрированы все теги и фильтры. Поэтому в верхней части вашего модуля поместите следующее:
from django import template register = template.Library()
В качестве альтернативы, модули тегов шаблонов могут быть зарегистрированы с помощью аргумента 'libraries' к DjangoTemplates. Это полезно, если вы хотите использовать другое имя для модуля тегов шаблонов при загрузке тегов шаблонов. Это также позволяет регистрировать теги без установки приложения.
За кулисами
Для получения множества примеров прочтите исходный код стандартных фильтров и тегов Django. Они находятся в django/template/defaultfilters.py и django/template/defaulttags.py, соответственно.
Для получения дополнительной информации о теге load ознакомьтесь с его документацией.
Создание настраиваемых фильтров шаблонов
Настраиваемые фильтры — это просто функции Python, которые принимают один или два аргумента:
- Значение переменной (вход) — не обязательно строка.
- Значение аргумента — это может иметь значение по умолчанию или быть опущенным.
Например, в фильтре {{ var|foo:"bar" }}, фильтру foo будет передан параметр var и аргумент "bar".
Так как язык шаблонов не предоставляет обработку исключений, любые исключения, поднятые фильтром шаблона, будут отображены как ошибка сервера. Таким образом, функции фильтров должны избегать вызова исключений, если есть разумное значение по умолчанию, которое можно вернуть. В случае входных данных, представляющих явную ошибку в шаблоне, вызов исключения может быть предпочтительнее, чем скрытая ошибка, которая скрывает эту ошибку.
Вот пример определения фильтра:
def cut(value, arg):
"""Removes all values of arg from the given string"""
return value.replace(arg, '')
И вот пример использования этого фильтра:
{{ somevariable|cut:"0" }}
Большинство фильтров не принимают аргументов. В этом случае просто опустите аргумент из своей функции. Пример:
def lower(value): # Only one argument.
"""Converts a string into all lowercase"""
return value.lower()
Регистрация настраиваемых фильтров
-
django.template.Library.filter()
После написания определения вашего фильтра, вам нужно зарегистрировать его с вашим экземпляром Library, чтобы он стал доступен языку шаблонов Django:
register.filter('cut', cut)
register.filter('lower', lower)
Метод Library.filter() принимает два аргумента:
- Имя фильтра — строка.
- Функция компиляции — функция Python (а не имя функции в виде строки).
Вы можете использовать register.filter() в качестве декоратора вместо этого:
@register.filter(name='cut')
def cut(value, arg):
return value.replace(arg, '')
@register.filter
def lower(value):
return value.lower()
Если вы опустите аргумент name, как во втором примере выше, Django будет использовать имя функции в качестве имени фильтра.
Наконец, register.filter() также принимает три ключевых аргумента, is_safe, needs_autoescape, и expects_localtime. Эти аргументы описаны в фильтрах и автоматическом экранировании и фильтрах и часовых поясах ниже.
Фильтры шаблонов, ожидающие строки
-
django.template.defaultfilters.stringfilter()
Если вы пишете фильтр шаблона, который ожидает только строку в качестве первого аргумента, вы должны использовать декоратор stringfilter. Это преобразует объект в его строковое представление, прежде чем передать его в вашу функцию:
from django import template
from django.template.defaultfilters import stringfilter
register = template.Library()
@register.filter
@stringfilter
def lower(value):
return value.lower()
Таким образом, вы сможете передавать, например, целое число в этот фильтр, и это не вызовет AttributeError (потому что у целых чисел нет методов lower()).
Фильтры и автоматическое экранирование
При написании настраиваемого фильтра следует уделить внимание тому, как фильтр будет взаимодействовать с поведением автоматического экранирования Django. Обратите внимание, что внутри кода шаблона могут циркулировать три типа строк:
-
Необработанные строки — это родные типы Python
strилиunicode. При выводе они экранируются, если активна автоматическая обработка экранирования, и отображаются неизменными в противном случае. -
Безопасные строки — это строки, которые помечены как безопасные для дальнейшего экранирования в момент вывода. Любое необходимое экранирование уже выполнено. Они обычно используются для вывода, содержащего исходный HTML, который должен интерпретироваться как есть на стороне клиента.
Внутренне эти строки имеют тип
SafeBytesилиSafeText. Они имеют общий базовый классSafeData, поэтому вы можете проверить их, используя код, подобный этому:if isinstance(value, SafeData): # Do something with the "safe" string. ...
Код фильтра шаблона попадает в одну из двух ситуаций:
-
Ваш фильтр не добавляет в результат никаких небезопасных для HTML символов (
<,>,',"или&), которые не были уже присутствуют. В этом случае вы можете позволить Django позаботиться обо всех операциях автоматической экранизации. Всё, что вам нужно сделать, это установить флагis_safeв значениеTrueпри регистрации вашей функции фильтра, например:@register.filter(is_safe=True) def myfilter(value): return valueЭтот флаг сообщает Django, что если в ваш фильтр передаётся «безопасная» строка, результат останется «безопасным», а если передаётся небезопасная строка, Django автоматически экранирует её, если необходимо.
Вы можете рассматривать это как «этот фильтр безопасен – он не создаёт возможности появления небезопасного HTML».
Причина, по которой
is_safeнеобходима, заключается в том, что существует множество обычных операций со строками, которые преобразуют объектSafeDataобратно в обычный объектstrилиunicode, и, вместо того, чтобы пытаться поймать их все, что было бы очень сложно, Django исправляет повреждения после завершения работы фильтра.Например, предположим, что у вас есть фильтр, который добавляет строку
xxв конец любого входного значения. Поскольку это не вводит опасных символов HTML в результат (кроме тех, которые уже присутствуют), вы должны пометить свой фильтр сis_safe:@register.filter(is_safe=True) def add_xx(value): return '%sxx' % valueКогда этот фильтр используется в шаблоне, где включена автоматическая экранизация, Django будет экранировать вывод всякий раз, когда входное значение не помечено как «безопасное».
По умолчанию
is_safeимеет значениеFalse, и вы можете опустить его из любых фильтров, где это не требуется.Будьте осторожны, принимая решение о том, действительно ли ваш фильтр оставляет безопасные строки безопасными. Если вы удаляете символы, вы можете случайно оставить не сбалансированные теги или сущности HTML в результате. Например, удаление
>из входных данных может превратить<a>в<a, что потребуется экранировать при выводе, чтобы избежать проблем. Аналогично, удаление точки с запятой (;) может преобразовать&в&, что больше не является допустимой сущностью и, следовательно, требует дополнительной экранизации. В большинстве случаев проблемы не будут столь сложными, но следите за подобными проблемами при проверке кода.Пометка фильтра
is_safeпринудительно преобразует значение возврата фильтра в строку. Если ваш фильтр должен возвращать булево значение или другое значение, отличное от строки, пометка егоis_safeможет привести к непредвиденным последствиям (например, преобразование булевого значения False в строку ‘False’). -
В качестве альтернативы, код вашего фильтра может вручную обрабатывать необходимую экранизацию. Это необходимо, когда вы добавляете новые HTML-разметки в результат. Вы хотите отметить вывод как безопасный от дальнейшей экранизации, чтобы ваша HTML-разметка не экранировалась дополнительно, поэтому вам нужно самостоятельно обработать входные данные.
Чтобы отметить вывод как безопасную строку, используйте
django.utils.safestring.mark_safe().Однако будьте осторожны. Вам нужно сделать больше, чем просто отметить вывод как безопасный. Вам необходимо убедиться, что он действительно безопасен, и то, что вы делаете, зависит от того, включена ли автоматическая экранизация. Идея заключается в том, чтобы писать фильтры, которые могут работать в шаблонах, где автоматическая экранизация включена или выключена, чтобы упростить работу авторам шаблонов.
Для того, чтобы ваш фильтр знал текущее состояние автоматической экранизации, установите флаг
needs_autoescapeв значениеTrueпри регистрации вашей функции фильтра. (Если вы не укажете этот флаг, он будет по умолчаниюFalse). Этот флаг сообщает Django, что ваша функция фильтра хочет получить дополнительный аргумент ключевого слова, называемыйautoescape, который имеет значениеTrueпри включенной автоматической экранизации иFalseв противном случае. Рекомендуется установить значение параметраautoescapeпо умолчанию вTrue, чтобы, если вы вызываете функцию из кода Python, экранизация была включена по умолчанию.Например, давайте напишем фильтр, который выделяет первый символ строки:
from django import template from django.utils.html import conditional_escape from django.utils.safestring import mark_safe register = template.Library() @register.filter(needs_autoescape=True) def initial_letter_filter(text, autoescape=True): first, other = text[0], text[1:] if autoescape: esc = conditional_escape else: esc = lambda x: x result = '<strong>%s</strong>%s' % (esc(first), esc(other)) return mark_safe(result)Флаги
needs_autoescapeи аргумент ключевого словаautoescapeозначают, что наша функция будет знать, включена ли автоматическая экранизация, когда вызывается фильтр. Мы используемautoescapeдля определения того, нужно ли передавать входные данные черезdjango.utils.html.conditional_escapeили нет. (В последнем случае мы просто используем функцию тождества в качестве функции «экранирования».) Функцияconditional_escape()подобнаescape(), за исключением того, что она экранирует только входные данные, которые не являются экземпляромSafeData. Если экземплярSafeDataпередаётся вconditional_escape(), данные возвращаются без изменений.Наконец, в приведённом примере мы помним, что нужно пометить результат как безопасный, чтобы наш HTML был вставлен непосредственно в шаблон без дополнительной экранизации.
В этом случае нет необходимости беспокоиться о флаге
is_safe(хотя включение его ничего не испортит). Всякий раз, когда вы вручную обрабатываете проблемы с автоматической экранизацией и возвращаете безопасную строку, флагis_safeтоже ничего не изменит.
Предупреждение
Предотвращение уязвимостей XSS при повторном использовании встроенных фильтров
Встроенные фильтры Django по умолчанию имеют autoescape=True для получения правильного поведения автоматической экранизации и предотвращения уязвимостей межсайтового скриптинга.
В более старых версиях Django будьте осторожны при повторном использовании встроенных фильтров Django, так как autoescape по умолчанию имеет значение None. Вам нужно будет передать autoescape=True для получения автоматической экранизации.
Например, если вы хотели написать пользовательский фильтр под названием urlize_and_linebreaks, который объединил фильтры urlize и linebreaksbr, фильтр будет выглядеть следующим образом:
from django.template.defaultfilters import linebreaksbr, urlize
@register.filter(needs_autoescape=True)
def urlize_and_linebreaks(text, autoescape=True):
return linebreaksbr(
urlize(text, autoescape=autoescape),
autoescape=autoescape
)
Затем:
{{ comment|urlize_and_linebreaks }}
будет эквивалентно:
{{ comment|urlize|linebreaksbr }}
Фильтры и часовые пояса
Если вы пишете пользовательский фильтр, который работает с объектами datetime, обычно вы регистрируете его с флагом expects_localtime установленным в значение True:
@register.filter(expects_localtime=True)
def businesshours(value):
try:
return 9 <= value.hour < 17
except AttributeError:
return ''
Когда этот флаг установлен, если первым аргументом вашего фильтра является объект datetime, учитывающий часовой пояс, Django преобразует его в текущий часовой пояс перед передачей его вашему фильтру, когда это уместно, согласно правилам преобразования часовых поясов в шаблонах.
Создание пользовательских тегов шаблонов
Простые теги
-
django.template.Library.simple_tag()
Многие теги шаблонов принимают несколько аргументов – строки или переменные шаблонов – и возвращают результат после обработки, основанной исключительно на входных аргументах и некоторой внешней информации. Например, тег current_time может принимать строку формата и возвращать время в виде отформатированной строки.
Для упрощения создания таких тегов Django предоставляет вспомогательную функцию simple_tag. Эта функция, являющаяся методом django.template.Library, принимает функцию, которая принимает любое количество аргументов, оборачивает её в функцию render и другие необходимые части, упомянутые выше, и регистрирует её в системе шаблонов.
Наша функция current_time могла бы быть записана так:
import datetime
from django import template
register = template.Library()
@register.simple_tag
def current_time(format_string):
return datetime.datetime.now().strftime(format_string)
Несколько замечаний о вспомогательной функции simple_tag:
- Проверка требуемого количества аргументов и т. д. уже выполняется к тому моменту, когда наша функция вызывается, поэтому нам не нужно это делать.
- Кавычки вокруг аргумента (если таковые имеются) уже удалены, поэтому мы получаем просто строку.
- Если аргумент был переменной шаблона, нашей функции передаётся текущее значение переменной, а не сама переменная.
В отличие от других утилит тегов, simple_tag пропускает свой вывод через conditional_escape(), если контекст шаблона находится в режиме автоэкранирования, чтобы обеспечить правильность HTML и защитить вас от уязвимостей XSS.
Если дополнительная экранизация не требуется, вам нужно использовать mark_safe(), если вы абсолютно уверены, что ваш код не содержит уязвимостей XSS. Для создания небольших фрагментов HTML рекомендуется использовать format_html() вместо mark_safe().
Была добавлена автоматическая экранизация для simple_tag, как описано в предыдущих двух абзацах.
Если вашему тегу шаблона нужно получить доступ к текущему контексту, вы можете использовать аргумент takes_context при регистрации вашего тега:
@register.simple_tag(takes_context=True)
def current_time(context, format_string):
timezone = context['timezone']
return your_get_current_time_method(timezone, format_string)
Обратите внимание, что первый аргумент обязательно должен называться context.
Дополнительную информацию о работе параметра takes_context см. в разделе включения тегов.
Если вам нужно переименовать свой тег, вы можете указать для него пользовательское имя:
register.simple_tag(lambda x: x - 1, name='minusone')
@register.simple_tag(name='minustwo')
def some_function(value):
return value - 2
Функции simple_tag могут принимать любое количество позиционных или ключевых аргументов. Например:
@register.simple_tag
def my_tag(a, b, *args, **kwargs):
warning = kwargs['warning']
profile = kwargs['profile']
...
return ...
Затем в шаблоне можно передать любое количество аргументов, разделённых пробелами, в тег шаблона. Как и в Python, значения для аргументов ключевых слов задаются с помощью знака равенства («=») и должны указываться после позиционных аргументов. Например:
{% my_tag 123 "abcd" book.title warning=message|lower profile=user.profile %}
Возможна ситуация, когда результаты тега сохраняются в переменную шаблона вместо непосредственного вывода. Это делается с помощью аргумента as, за которым следует имя переменной. Это позволяет выводить содержимое самостоятельно, где вам это нужно:
{% current_time "%Y-%m-%d %I:%M %p" as the_time %}
<p>The time is {{ the_time }}.</p>
Теги включения
-
django.template.Library.inclusion_tag()
Другой распространённый тип тега шаблона — тег, отображающий данные, рендеринг другого шаблона. Например, админский интерфейс Django использует пользовательские теги шаблонов для отображения кнопок внизу страниц форм «добавить/изменить». Эти кнопки всегда выглядят одинаково, но ссылки меняются в зависимости от редактируемого объекта — поэтому это идеальный случай для использования небольшого шаблона, заполняемого подробностями текущего объекта. (В случае админа это тег submit_row)
Такие теги называются «тегами включения».
Написание тегов включения, вероятно, лучше всего продемонстрировать на примере. Давайте напишем тег, который выводит список вариантов для данного Poll объекта, такого, как был создан в учебниках. Мы будем использовать тег так:
{% show_results poll %}
…и вывод будет чем-то вроде этого:
<ul> <li>First choice</li> <li>Second choice</li> <li>Third choice</li> </ul>
Сначала определите функцию, которая принимает аргумент и генерирует словарь данных для результата. Важно то, что нам нужно вернуть только словарь, а не что-то более сложное. Это будет использоваться в качестве контекста шаблона для фрагмента шаблона. Пример:
def show_results(poll):
choices = poll.choice_set.all()
return {'choices': choices}
Далее создайте шаблон, используемый для рендеринга вывода тега. Этот шаблон является фиксированной частью тега: его определяет автор тега, а не разработчик шаблона. Следуя нашему примеру, шаблон очень прост:
<ul>
{% for choice in choices %}
<li> {{ choice }} </li>
{% endfor %}
</ul>
Теперь создайте и зарегистрируйте тег включения, вызвав метод inclusion_tag() на объекте Library. Следуя нашему примеру, если указанный выше шаблон находится в файле results.html в каталоге, который ищется загрузчиком шаблонов, мы зарегистрируем тег так:
# Here, register is a django.template.Library instance, as before
@register.inclusion_tag('results.html')
def show_results(poll):
...
В качестве альтернативы можно зарегистрировать тег включения, используя экземпляр django.template.Template:
from django.template.loader import get_template
t = get_template('results.html')
register.inclusion_tag(t)(show_results)
…при первом создании функции.
Иногда ваши теги включения могут потребовать большого количества аргументов, что затрудняет авторам шаблонов передавать все аргументы и запоминать их порядок. Для решения этой проблемы Django предоставляет параметр takes_context для тегов включения. Если вы укажете takes_context при создании тега шаблона, тег не будет иметь обязательных аргументов, а функция на стороне Python будет иметь один аргумент — контекст шаблона на момент вызова тега.
Например, предположим, что вы пишете тег включения, который всегда будет использоваться в контексте, содержащем home_link и home_title переменные, которые указывают на главную страницу. Вот как будет выглядеть функция Python:
@register.inclusion_tag('link.html', takes_context=True)
def jump_link(context):
return {
'link': context['home_link'],
'title': context['home_title'],
}
Обратите внимание, что первый параметр функции обязательно должен называться context.
В этой register.inclusion_tag() строке мы указали takes_context=True и имя шаблона. Вот как может выглядеть шаблон link.html.
Jump directly to <a href="{{ link }}">{{ title }}</a>.
Затем, всякий раз, когда вы хотите использовать этот пользовательский тег, загрузите его библиотеку и вызовите без аргументов, как показано ниже:
{% jump_link %}
Обратите внимание, что при использовании takes_context=True, нет необходимости передавать аргументы тегу шаблона. Он автоматически получает доступ к контексту.
Параметр takes_context по умолчанию равен False. Если он установлен на True, тегу передаётся объект контекста, как в этом примере. Это единственное отличие от предыдущего примера inclusion_tag.
Функции inclusion_tag могут принимать любое количество позиционных или ключевых аргументов. Например:
@register.inclusion_tag('my_template.html')
def my_tag(a, b, *args, **kwargs):
warning = kwargs['warning']
profile = kwargs['profile']
...
return ...
Затем в шаблоне может быть передано любое количество аргументов, разделённых пробелами, тегу шаблона. Как и в Python, значения ключевых аргументов устанавливаются с помощью знака равенства («=») и должны быть указаны после позиционных аргументов. Например:
{% my_tag 123 "abcd" book.title warning=message|lower profile=user.profile %}
Краткое описание тегов присваивания
-
django.template.Library.assignment_tag()
Устарело начиная с версии 1.9: simple_tag теперь может хранить результаты в переменной шаблона и должно использоваться вместо него.
Для облегчения создания тегов, устанавливающих переменную в контексте, Django предоставляет вспомогательную функцию assignment_tag. Эта функция работает так же, как simple_tag(), за исключением того, что она хранит результат тега в указанной переменной контекста вместо непосредственного вывода.
Наша предыдущая функция current_time могла быть записана следующим образом:
@register.assignment_tag
def get_current_time(format_string):
return datetime.datetime.now().strftime(format_string)
Затем вы можете сохранить результат в переменной шаблона, используя аргумент as, за которым следует имя переменной, и вывести его в нужном месте:
{% get_current_time "%Y-%m-%d %I:%M %p" as the_time %}
<p>The time is {{ the_time }}.</p>
Расширенные пользовательские теги шаблонов
Краткое описание
Система шаблонов работает в двухэтапном процессе: компиляции и рендеринга. Чтобы определить пользовательский тег шаблона, вы указываете, как работает компиляция и как работает рендеринг.
Когда Django компилирует шаблон, он разбивает исходный текст шаблона на «узлы». Каждый узел является экземпляром django.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’
Узел 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/1.10/howto/custom-template-tags/