Как создать пользовательские теги и фильтры шаблонов
Язык шаблонов Django поставляется с широким набором встроенных тегов и фильтров, разработанных для удовлетворения потребностей вашей приложения в логике представления. Тем не менее, вам может потребоваться функциональность, которой не охватывает базовый набор примитивов шаблонов. Вы можете расширить движок шаблонов, определив пользовательские теги и фильтры с помощью Python, а затем сделать их доступными для ваших шаблонов, используя тег {% load %}.
Структура кода
Наиболее распространенным местом для указания пользовательских тегов и фильтров шаблонов является внутри приложения Django. Если они относятся к существующему приложению, имеет смысл объединить их там; в противном случае их можно добавить в новое приложение. Когда приложение Django добавляется в INSTALLED_APPS, любые теги, определенные им в стандартном расположении, описанном ниже, автоматически становятся доступными для загрузки в шаблонах.
Приложение должно содержать каталог templatetags, на том же уровне, что и models.py, views.py и т. д. Если он еще не существует, создайте его — не забудьте файл __init__.py, чтобы убедиться, что каталог обрабатывается как пакет Python.
Сервер разработки не будет автоматически перезапускаться
После добавления модуля templatetags вам необходимо перезапустить сервер, прежде чем вы сможете использовать теги или фильтры в шаблонах.
Ваши пользовательские теги и фильтры будут храниться в модуле внутри каталога templatetags. Имя файла модуля — это имя, которое вы будете использовать для загрузки тегов позже, поэтому будьте внимательны, чтобы не выбрать имя, которое конфликтует с пользовательскими тегами и фильтрами в другом приложении.
Например, если ваши пользовательские теги/фильтры находятся в файле poll_extras.py, структура вашего приложения может выглядеть так:
polls/
__init__.py
models.py
templatetags/
__init__.py
poll_extras.py
views.py
И в вашем шаблоне вы будете использовать следующее:
{% load poll_extras %}
Приложение, содержащее пользовательские теги, должно находиться в INSTALLED_APPS, чтобы тег {% load %} работал. Это функция безопасности: она позволяет размещать код Python для многих библиотек шаблонов на одном хост-машине без предоставления доступа ко всем из них для каждой установки Django.
Нет ограничений на количество модулей, которые вы можете поместить в пакет templatetags. Просто помните, что оператор {% load %} загрузит теги/фильтры для данного имени модуля Python, а не имени приложения.
Чтобы быть допустимой библиотекой тегов, модуль должен содержать переменную уровня модуля с именем register, которая является экземпляром template.Library, в котором зарегистрированы все теги и фильтры. Поэтому в начале вашего модуля поместите следующее:
from django import template register = template.Library()
В качестве альтернативы, модули тегов шаблонов можно зарегистрировать через аргумент 'libraries' к DjangoTemplates. Это полезно, если вы хотите использовать другое имя для загрузки тегов шаблонов, отличное от имени модуля тега шаблона. Это также позволяет регистрировать теги без установки приложения.
С технической точки зрения
Для получения множества примеров обратитесь к исходному коду стандартных фильтров и тегов Django. Они находятся в django/template/defaultfilters.py и django/template/defaulttags.py, соответственно.
Для получения дополнительной информации о теге load обратитесь к его документации.
Написание пользовательских фильтров шаблонов
Пользовательские фильтры — это функции Python, которые принимают один или два аргумента:
- Значение переменной (вход) — необязательно строка.
- Значение аргумента — может иметь значение по умолчанию или быть опущенным.
Например, в фильтре {{ var|foo:"bar" }}, фильтру foo будет передано значение переменной var и аргумента "bar".
Поскольку язык шаблонов не предоставляет обработку исключений, любое исключение, поднятое пользовательским фильтром шаблона, будет отображено как ошибка сервера. Поэтому функции фильтров должны избегать подъёма исключений, если есть разумное значение по умолчанию для возврата. В случае входных данных, которые представляют явную ошибку в шаблоне, поднятие исключения может быть лучше, чем скрытая ошибка, которая скрывает ошибку.
Вот пример определения фильтра:
def cut(value, arg):
"""Removes all values of arg from the given string"""
return value.replace(arg, "")
И вот пример того, как этот фильтр будет использоваться:
{{ somevariable|cut:"0" }}
Большинство фильтров не принимают аргументов. В этом случае опустите аргумент из вашей функции:
def lower(value): # Only one argument.
"""Converts a string into all lowercase"""
return value.lower()
Регистрация пользовательских фильтров
-
django.template.Library.filter()
После написания определения фильтра вам необходимо зарегистрировать его в вашем экземпляре Library, чтобы сделать его доступным для языка шаблонов Django:
register.filter("cut", cut)
register.filter("lower", lower)
Метод Library.filter() принимает два аргумента:
- Имя фильтра — строка.
- Функция компиляции — функция Python (а не имя функции как строка).
Вы можете использовать register.filter() вместо этого:
@register.filter(name="cut")
def cut(value, arg):
return value.replace(arg, "")
@register.filter
def lower(value):
return value.lower()
Если вы опустите аргумент name, как и во втором примере выше, Django будет использовать имя функции в качестве имени фильтра.
Наконец, register.filter() также принимает три именованных аргумента, is_safe, needs_autoescape и expects_localtime. Эти аргументы описаны в фильтрах и автоматическом экранировании и фильтрах и часовых поясах ниже.
Фильтры шаблонов, ожидающие строки
-
django.template.defaultfilters.stringfilter()
Если вы пишете фильтр шаблона, который ожидает только строку в качестве первого аргумента, вы должны использовать декоратор stringfilter. Это преобразует объект в его строковое значение перед передачей в вашу функцию:
from django import template
from django.template.defaultfilters import stringfilter
register = template.Library()
@register.filter
@stringfilter
def lower(value):
return value.lower()
Таким образом, вы сможете передавать, например, целое число в этот фильтр, и это не вызовет AttributeError (потому что целые числа не имеют lower() методов).
Фильтры и автоматическое экранирование
При написании пользовательского фильтра подумайте о том, как фильтр будет взаимодействовать с поведением автоматического экранирования Django. Обратите внимание, что внутри кода шаблона могут циркулировать два типа строк:
- Необработанные строки — это обычные строки Python. При выводе они экранируются, если автоматическое экранирование включено, в противном случае выводятся без изменений.
-
Безопасные строки — это строки, которые были помечены как безопасные от дальнейшего экранирования во время вывода. Любое необходимое экранирование уже выполнено. Они обычно используются для вывода, содержащего необработанный 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 ""
Когда этот флаг установлен, если первым аргументом вашего фильтра является объект datetime, осознающий часовой пояс, Django преобразует его в текущий часовой пояс перед передачей его вашему фильтру, когда это необходимо, в соответствии с правилами преобразования часовых поясов в шаблонах.
Создание пользовательских тегов шаблонов
Теги сложнее, чем фильтры, потому что теги могут делать всё. 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.simple_block_tag()
Когда необходимо передать часть отрисованного шаблона в пользовательский тег, Django предоставляет вспомогательную функцию simple_block_tag для этого. Подобно simple_tag(), эта функция принимает пользовательскую функцию тега, но с дополнительным аргументом content, который содержит отформатированное содержимое, как определено внутри тега. Это позволяет легко включать динамические разделы шаблонов в пользовательские теги.
Например, пользовательский тег блока, который создаёт диаграмму, может выглядеть так:
from django import template
from myapp.charts import render_chart
register = template.Library()
@register.simple_block_tag
def chart(content):
return render_chart(source=content)
Аргумент content содержит всё между тегами {% chart %} и {% endchart %}:
{% chart %}
digraph G {
label = "Chart for {{ request.user }}"
A -> {B C}
}
{% endchart %}
Если внутри блока content есть другие теги или переменные шаблонов, они будут отрисованы перед передачей в функцию тега. В примере выше, request.user будет разрешён к моменту вызова render_chart.
Теги блока закрываются с помощью end{name} (например, endchart). Это можно настроить параметром end_name:
@register.simple_block_tag(end_name="endofchart")
def chart(content):
return render_chart(source=content)
Что потребовало бы определения шаблона такого вида:
{% chart %}
digraph G {
label = "Chart for {{ request.user }}"
A -> {B C}
}
{% endofchart %}
Несколько замечаний о simple_block_tag:
- Первый аргумент должен называться
content, и он будет содержать содержимое тега шаблона в виде отформатированной строки. - Переменные, переданные тегу, не включаются в контекст рендеринга содержимого, как это было бы при использовании тега
{% with %}.
Так же, как и simple_tag, simple_block_tag:
- Проверяет количество и качество аргументов.
- Удаляет кавычки из аргументов при необходимости.
- Экранирует вывод соответствующим образом.
- Поддерживает передачу
takes_context=Trueпри регистрации для доступа к контексту. Обратите внимание, что в этом случае первый аргумент пользовательской функции обязательно должен называтьсяcontext, аcontentдолжен следовать за ним. - Поддерживает переименование тега путём передачи аргумента
nameпри регистрации. - Поддерживает приём любого количества позиционных или именованных аргументов.
- Поддерживает хранение результата в переменной шаблона с помощью варианта
as.
Экранирование содержимого
simple_block_tag ведет себя аналогично simple_tag в отношении автоматической экранизации. Подробнее об экранизации и безопасности см. в simple_tag. Так как аргумент content уже был отрисован Django, он уже экранирован.
Полный пример
Рассмотрим пользовательский тег шаблона, генерирующий окно сообщения, поддерживающее несколько уровней сообщений и контент, выходящий за рамки простой фразы. Это можно реализовать с помощью simple_block_tag следующим образом:
testapp/templatetags/testapptags.pyfrom django import template
from django.utils.html import format_html
register = template.Library()
@register.simple_block_tag(takes_context=True)
def msgbox(context, content, level):
format_kwargs = {
"level": level.lower(),
"level_title": level.capitalize(),
"content": content,
"open": " open" if level.lower() == "error" else "",
"site": context.get("site", "My Site"),
}
result = """
<div class="msgbox {level}">
<details{open}>
<summary>
<strong>{level_title}</strong>: Please read for <i>{site}</i>
</summary>
<p>
{content}
</p>
</details>
</div>
"""
return format_html(result, **format_kwargs)
В сочетании с минимальной функцией представления и соответствующим шаблоном, как показано здесь:
testapp/views.pyfrom django.shortcuts import render
def simpleblocktag_view(request):
return render(request, "test.html", context={"site": "Important Site"})
testapp/templates/test.html{% extends "base.html" %}
{% load testapptags %}
{% block content %}
{% msgbox level="error" %}
Please fix all errors. Further documentation can be found at
<a href="http://example.com">Docs</a>.
{% endmsgbox %}
{% msgbox level="info" %}
More information at: <a href="http://othersite.com">Other Site</a>/
{% endmsgbox %}
{% endblock %}
Следующий HTML создаётся в качестве результата рендеринга:
<div class="msgbox error">
<details open>
<summary>
<strong>Error</strong>: Please read for <i>Important Site</i>
</summary>
<p>
Please fix all errors. Further documentation can be found at
<a href="http://example.com">Docs</a>.
</p>
</details>
</div>
<div class="msgbox info">
<details>
<summary>
<strong>Info</strong>: Please read for <i>Important Site</i>
</summary>
<p>
More information at: <a href="http://othersite.com">Other Site</a>
</p>
</details>
</div>
Теги включения
-
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 компилирует шаблон, он разделяет исходный текст шаблона на «узлы». Каждый узел — это экземпляр 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/5.2/howto/custom-template-tags/