Django содержит большое количество встроенных тегов и фильтров. Тем не менее, вам может понадобиться добавить собственный функционал к шаблонам. Вы можете сделать это добавив собственную библиотеку тегов и фильтров используя Python, затем добавить ее в шаблон с помощью тега {% load %}.
Собственные теги и фильтры шаблонов должны определяться в приложении Django. Если они логически связаны с каким-то приложением, есть смысл добавить их в это приложение, иначе создайте новое приложение.
Приложение должно содержать каталог templatetags на том же уровне что и models.py, views.py и др. Если он не существует, создайте его. Не забудьте создать файл __init__.py что бы каталог мог использоваться как пакет Python.
Выше теги и фильтры будут находиться в модуле пакета templatetags. Название модуля будет использоваться при загрузке библиотеки в шаблоне, так что убедитесь что оно не совпадает с названиями библиотек других приложений.
Например, если теги/фильтры находятся в файле poll_extras.py, ваше приложение может выглядеть следующим образом:
polls/
models.py
templatetags/
__init__.py
poll_extras.py
views.py
И в шаблоне вы будут использовать:
{% load poll_extras %}
Приложение содержащее собственные теги и фильтры должно быть добавлено в INSTALLED_APPS, что бы тег {% load %} мог загрузить его. Это сделано в целях безопасности.
Не имеет значение сколько модулей добавлено в пакет templatetags. Помните что тег {% load %} использует название модуля, а не название приложения.
Библиотека тегов должна содержать переменную register равную экземпляру django.template.Library, в которой регистрируются все определенные теги и фильтры. Так что в начале вашего модуля укажите следующие строки:
from django import template
register = template.Library()
Behind the scenes
Вы можете найти большое количество примеров в исходном коде встроенных тегов и фильтров 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()
Создав функцию фильтра, ее необходимо зарегистрировать в экземпляре Library, чтобы использовать в шаблоне:
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, описанные в разделе фильтры и автоматическое экранирование ниже.
Если вы создали фильтр, который работает только со строками, используйте декоратор 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, которые необходимо отобразить на странице.
Внутренне эти строки представлены типами SafeString или SafeUnicode. Эти типа наследуются от базового класса SafeData, таким образом вы можете проверять их следующим образом:
if isinstance(value, SafeData):
# Do something with the "safe" string.
...
Строки с пометкой “требуют экранирования” – всегда экранируются при выводе, независимо от того находятся они в блоке autoescape или нет. Такие строки экранируются только один раз, независимо от того, включено автоматическое экранирование или нет.
Внутренне эти строки представлены типами EscapeString или EscapeUnicode. Вам не обязательно это знать, можно просто использовать фильтр escape.
При создании фильтра вы можете столкнуться со следующими ситуациями:
Ваш фильтр не добавлять никаких не экранированных HTML-символов (<, >, ', " or &) в результат. В таком случае вы можете полностью положиться на политику автоматического экранирования Django. Для этого передайте параметр is_safe с значением True при регистрации функции фильтра:
@register.filter(is_safe=True)
def myfilter(value):
return value
Этот параметр указывает Django что фильтр никак не изменяет “безопасность” переданной строки. То есть, если передать в фильтр “безопасную” строку, результат так же будет “безопасным” для 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 теги или сущности(entities) в результате. Например, при удалении > из входящих данных <a> может превратиться в <a, который должен быть экранирован. Аналогично, удаление точки с запятой (;) может превратить & в &, что не является правильной HTML-сущностью и должно быть экранировано. Большинство случаев будут не такими сложными, но вы должны быть внимательными.
Параметр is_safe принуждает фильтр вернуть строку. Если ваш фильтр возвращает булево значение или не строку, использование is_safe может привести к непредвиденным последствиям (например, конвертирование False в строку ‘False’).
Фильтр самостоятельно заботиться об экранировании результата. Это необходимо если вы добавляете новый HTML в результат. В таком случае результат должен быть помечен как безопасный что бы избежать последующего экранирования.
Для этого используйте функцию django.utils.safestring.mark_safe().
Но будьте осторожны. Необходимо не просто пометить результат как безопасный. Вы должны убедиться что результат действительно безопасен независимо от того, включено автоматическое экранирование или нет. Идея в том, чтобы фильтр правильно работал как при включенном автоматическом экранировании так и выключенном.
Для того, чтобы фильтр знал включено ли автоматическое экранирование, передайте параметр needs_autoescape со значением True при регистрации функций фильтра. (По умолчанию значение равно False). Этот параметр указывает Django что необходимо передать именованный аргумент autoescape при вызове функции фильтра, который равен True, если включено автоматическое экранирование.
Например, давайте создадим фильтр, который выделяет первый символ строки:
from django.utils.html import conditional_escape
from django.utils.safestring import mark_safe
@register.filter(needs_autoescape=True)
def initial_letter_filter(text, autoescape=None):
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 для входящих данных. (В нашем примере мы использовали его для определения функции “escape”.) Функция conditional_escape() как и escape(), но использует экранирование только для не безопасных(SafeData) строк. Если передать объект SafeData функция conditional_escape() вернет его без изменений.
Так же мы пометили результат как безопасный и он будет вставлен непосредственно в шаблон без повторного экранирования.
В этом случае нет необходимости беспокоиться о параметре is_safe. Так как вы самостоятельно учитываете автоматическое экранирование, параметр is_safe ничего не изменит.
is_safe и needs_autoescape раньше использовались как атрибуты функции, такой подход устарел.
@register.filter
def myfilter(value):
return value
myfilter.is_safe = True
@register.filter
def initial_letter_filter(text, autoescape=None):
# ...
return mark_safe(result)
initial_letter_filter.needs_autoescape = True
Если вы создаете фильтр, который обрабатывает объекты datetime, скорее всего вы будете использовать параметр expects_localtime со значением True:
@register.filter(expects_localtime=True)
def businesshours(value):
try:
return 9 <= value.hour < 17
except AttributeError:
return ''
Если этот флаг установлен и первый аргумент дата с временной зоной, Django преобразует ее в текущую временную зону перед тем, как передать в фильтр, в соответствии с правилами преобразования временных зон в шаблоне.
Теги сложнее чем фильтры, так как они могут делать что угодно.
Уже описывалось что система шаблонов работает в два этапа: компиляция и выполнение. Создавая собственный тег вы определяете как выполняется компиляция и выполнение тега.
Когда Django компилирует шаблон, содержимое шаблона разбивается на ‘’узлы’‘(nodes). Каждый узел это экземпляр 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 <http://ru.wikipedia.org/wiki/%D0%A0%D0%B0%D1%81%D1%88%D0%B8%D1%80%D0%B5%D0%BD%D0%BD%D0%B0%D1%8F_%D1%84%D0%BE%D1%80%D0%BC%D0%B0_%D0%91%D1%8D%D0%BA%D1%83%D1%81%D0%B0_%E2%80%94_%D0%9D%D0%B0%D1%83%D1%80%D0%B0>`_, но эти эксперименты делали систему шаблонов медленной. Парсер низкоуровневый, так как это делает его быстрым.
Следующим этапом мы создаем подкласс Node с методом render().
В продолжение нашего примера создадим класс CurrentTimeNode:
from django import template
import datetime
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() не должен вызывать исключение TemplateSyntaxError или любое другое. Все ошибки должны игнорироваться как и в фильтре.
Разделение компиляции и выполнения эффективно так как позволяет выполнить шаблон с несколькими контекстами без надобности выполнять парсинг каждый раз.
Вывод тега не экранируется. Однако, есть несколько вещей которые следует помнить.
Если метод render() добавляет переменную в контекст (вместо того, что бы вернуть строку), он должен пометить ее как безопасную используя функцию mark_safe(), если это необходимо. В конечном итоге к переменной будет применяться автоматическое экранирование при выводе в шаблоне, так что необходимо пометить ее как безопасную что бы избежать повторного экранирования значения.
Если тег создает новый контекст, необходимо установить параметр автоматического экранирования со значением текущего контекста. Метод __init__ класса Context принимает аргумент ``autoescape`, который вы можете использовать. Например:
def render(self, context):
# ...
new_context = Context({'var': obj}, autoescape=context.autoescape)
# ... Do something with new_context ...
Это не совсем обычная ситуация, но может быть полезно если вы самостоятельно выполняете шаблон. Например:
def render(self, context):
t = template.loader.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 могла бы выглядеть следующим образом:
class CycleNode(Node):
def __init__(self, cyclevars):
self.cycle_iter = itertools.cycle(cyclevars)
def render(self, context):
return self.cycle_iter.next()
Однако, предположим что одновременно выполняется два экземпляра шаблона представленного выше:
Поток 1 выполняет первую итерацию по циклу, CycleNode.render() возвращает ‘row1’
Поток 2 выполняет первую итерацию по циклу, CycleNode.render() возвращает ‘row2’
Поток 1 выполняет вторую итерацию по циклу, CycleNode.render() возвращает ‘row1’
Поток 2 выполняет вторую итерацию по циклу, CycleNode.render() возвращает ‘row2’
CycleNode работает, но итерация происходит глобально. Так как Поток 1 и Поток 2 связаны, они используют одни значения. Это точно не то, что вам нужно!
Для решения этой проблемы Django предоставляет render_context в контексте текущего шаблона. render_context работает как и словарь в Python и должен использоваться для хранения состояния узлов между вызовами метода render.
Давайте перепишем CycleNode что бы использовать render_context:
class CycleNode(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 cycle_iter.next()
Заметим, что вполне безопасно сохранять в атрибутах объекта Node информацию, которая не изменяется. В случае CycleNode, параметр cyclevars не изменяется после создания экземпляра Node, и нет необходимости хранить его в render_context. Но информация, которая относится к конкретному шаблону, например текущая итерация узла CycleNode, должна сохраняться в render_context.
Примечание
Обратите внимание как мы используем self для привязки состояния к текущему узлу в render_context. В шаблоне может быть несколько CycleNode, и важно не нарушить состояние других узлов. Самый просто способ это использовать 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])
Теперь вам следует изменить метод render узла что бы получить значение атрибута 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 если невозможно найти значение переменной в текущем контексте.
Большинство тегов принимают определенное количество аргументов – строки или переменные шаблона – и возвращают строку после обработки аргументов. Например, тег current_time, который мы написали, один из них.
Что бы упростить создание подобных тегов, Django предоставляет функцию simple_tag. Эта функция, которая является методом django.template.Library, принимает функцию принимающую любое количество аргументов, оборачивает функцией render и регистрирует в системе шаблонов.
Функцию current_time можно переписать следующим образом:
def current_time(format_string):
return datetime.datetime.now().strftime(format_string)
register.simple_tag(current_time)
Можно использовать как декоратор:
@register.simple_tag
def current_time(format_string):
...
Несколько вещей которые следует помнить о функции simple_tag:
Проверка количества обязательных аргументов и др. выполняется до вызова функции, вам не нужно этого делать.
Кавычки вокруг строк уже удалены, аргумент будет содержать готовую строку.
Если аргумент является переменной шаблона, наша функция получит ее значение.
Если тегу необходим текущий контекст, используйте параметр takes_context при регистрации тега:
# The first argument *must* be called "context" here.
def current_time(context, format_string):
timezone = context['timezone']
return your_get_current_time_method(timezone, format_string)
register.simple_tag(takes_context=True)(current_time)
Или:
@register.simple_tag(takes_context=True)
def current_time(context, format_string):
timezone = context['timezone']
return your_get_current_time_method(timezone, format_string)
Подробности о параметре 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 использует включающий тег для отображения кнопок под формой на страницах добавления/редактирования объектов. Эти кнопки выглядят всегда одинаково, но ссылки зависят от текущего объекта – небольшой шаблон, который выполняется с данными из текущего объекта, удобно использовать в данном случае. (В приложении администратора это тег 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}
Теперь создадим шаблон, который будет использовать для генерации результата. Этот шаблон полностью относится к тегу: создатель тега определяет его, не создатель шаблонов(template designer). Для нашего примера шаблон будет очень простым:
<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')(show_results)
Alternatively it is possible to register the inclusion tag using a django.template.Template instance:
from django.template.loader import get_template
t = get_template('results.html')
register.inclusion_tag(t)(show_results)
Можно использовать и как декоратор:
@register.inclusion_tag('results.html')
def show_results(poll):
...
...при создании функции.
В некотором случаях тег может требовать большого количества параметров. Может быть проблематично запомнить все параметры и их порядок. Что бы решить эту проблему Django предоставляет параметр takes_context для включающего тега. Если вы укажите takes_context при создании тега, тег не будет содержать обязательные аргумент, а функция Python будет принимать один аргумент – контекст текущего шаблона.
Например, предположим вы создаете тег, который будет использоваться в шаблонах с контекстом всегда содержащим переменные home_link и home_title. Вот как может выглядеть такой тег:
# The first argument *must* be called "context" here.
def jump_link(context):
return {
'link': context['home_link'],
'title': context['home_title'],
}
# Register the custom tag as an inclusion tag with takes_context=True.
register.inclusion_tag('link.html', takes_context=True)(jump_link)
(Заметим, что первый параметр должен называться 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 может принимать любое количество позиционных и именованных аргументов. Например:
@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 %}
Предыдущие примеры тегов просто выводят значение. Более гибкий способ - это добавить значение в переменную контекста вместо вывода результата. Таким образом автор шаблона может использовать результат выполнения тега несколько раз.
Что бы добавить переменную в контекст, просто добавьте значение в контекст как в словарь в методе render() объекта Node. Вот обновленная версия CurrentTimeNode, которая устанавливает переменную current_time вместо вывода результата:
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>
Variable scope in context
Любая переменная добавленная в контекст будет доступна только в блоке(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:
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 ''
import re
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.
Если вам нужно только добавить переменную в контекст, воспользуйтесь присваивающий тег(assignment tag).
Для простого создания тегов, которые добавляют переменную в контекст, Django предоставляет функцию assignment_tag. Эта функция работает так же, как и simple_tag, за исключением, что она добавляет результат в контекст, а не возвращает его.
Функцию current_time можно переписать следующим образом:
def get_current_time(format_string):
return datetime.datetime.now().strftime(format_string)
register.assignment_tag(get_current_time)
Можно использовать как декоратор:
@register.assignment_tag
def get_current_time(format_string):
...
Вы можете добавить результат в переменную шаблона используя аргумент as и название переменной, и использовать его при необходимости:
{% get_current_time "%Y-%m-%d %I:%M %p" as the_time %}
<p>The time is {{ the_time }}.</p>
Если тегу необходим текущий контекст, используйте параметр takes_context при регистрации тега:
# The first argument *must* be called "context" here.
def get_current_time(context, format_string):
timezone = context['timezone']
return your_get_current_time_method(timezone, format_string)
register.assignment_tag(takes_context=True)(get_current_time)
Или:
@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)
Подробности о параметре 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 %}
Шаблонные теги могут работать вместе. Например, встроенный тег {% 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().
Более сложные примеры ищите в исходном коде реализации {% if %}, {% for %}, {% ifequal %} или {% ifchanged %}. Они находятся django/template/defaulttags.py.
Mar 30, 2016