Теперь, когда вы знаете немного больше о внутренностях шаблонной системы, давайте рассмотрим как можно расширить её возможности с помощью дополнительного кода.
Большая часть работы над шаблонами производится в шаблонных тегах и/или фильтрах. Несмотря на то, что шаблонный язык Django поставляется со множеством встроенных тегов и фильтров, скорее всего вы будете создавать свои собственные библиотеки тегов и фильтров, которые будут удовлетворять вашим требованиям. К счастью, это не так сложно сделать.
При разработке своих тегов или фильтров, первое, что надо сделать — создать шаблонную библиотеку, это небольшая инфраструктура, которую может использовать Django.
Создание шаблонной библиотеки состоит из двух шагов:
Во-первых, определитесь какое приложение Django должно будет содержать шаблонную библиотеку. Если вы создали приложение с помощью python manage.py startapp, вы можете расположить её там или вы можете создать другое приложение, полностью ориентированное на хранение библиотеки.
Независимо от вашего выбора, следует удостовериться, что вы добавили приложение в параметр
INSTALLED_APPS. Мы ещё вернёмся к этому.Во-вторых, создайте каталог
templatetagsв соответствующем пакете приложения Django. Он должен быть на том же уровне, что и файлыmodels.py,views.pyи так далее. Например:books/ __init__.py models.py templatetags/ views.pyСоздайте два пустых файла в этом каталоге: файл
__init__.py, чтобы интерпретатор Python знал, что это пакет для него; и файл, который будет содержать определения ваших тегов и фильтров. Имя последнего файла можете выбрать сами. Например, если ваши теги и фильтры находятся в файлеpoll_extras.py, вам придётся писать в шаблоне так:{% load poll_extras %}Тег {% load %} просматривает содержимое параметра
INSTALLED_APPSв файле конфигурации и просто разрешает загрузку шаблонных библиотек из установленных приложений. Это особенность предназначена для обеспечения безопасности, она позволяет размещать код Python во многих шаблонных библиотеках на одном компьютере без разрешения доступа ко всем им для каждой инсталляции Django.
Если вы разрабатываете шаблонную библиотеку, которая не связана ни с одной определённой моделью или представлением, то правильно и достаточно стандартно иметь пакет приложения, который содержит только пакет templatetags. Нет никакого ограничения на количество модулей, которые вы можете разместить в пакете templatetags. Просто не забывайте, что оператор {% load %} будет загружать теги и фильтры по имени данного модуля, а не по имени приложения.
После создания такого модуля, вам потребуется написать немного кода, в зависимости от того, тег вы реализуете или фильтр.
Для того, чтобы быть правильной библиотекой тегов, модуль
должен содержать в себе переменную
register, которая является экземпляром
template.Library. Этот экземпляр класса
Library является структурой данных, в
которой зарегистрированы все теги и фильтры. Таким образом,
ваш модуль должен начинаться примерно так:
from django import template
register = template.Library()
Замечание
Ряд хороших примеров по разработке тегов и фильтров как
обычно находится в исходном коде Django. Изучите файлы
django/template/defaultfilters.py и
django/template/defaulttags.py. Некоторые
приложения в django.contrib также содержат
шаблонные библиотеки.
После создания переменной register, вы
будете использовать её при создании шаблонных фильтров и
тегов.
Фильтры это обычные функции языка Python, которые могут принимать от одного до двух аргументов:
Значение переменной (вход);
Значение аргумента, который может иметь значение по умолчанию или не иметь.
Например, в фильтре {{ var|foo:"bar" }},
фильтру foo будет передано содержимое
переменной var и аргумент
"bar".
Функции фильтра должны всегда что-нибудь возвращать. Они не должны вызывать исключения, т.е., они должны тихо игнорировать ошибки. При наличии ошибки, они должны возвращать либо оригинальное значение, либо пустую строку, что привлечёт к себе внимание.
Пример определения фильтра:
def cut(value, arg):
"Удаляет все значения аргумента arg из строки value"
return value.replace(arg, '')
А это пример того, как можно использовать данный фильтр:
{{ somevariable|cut:"0" }}
Большинство фильтров не принимают аргументы. В этом случае, просто не используйте второй аргумент в вашей функции:
def lower(value): # Only one argument.
"Преобразовывает регистр строки в строчный"
return value.lower()
После определения своего фильтра требуется зарегистрировать
его в вашем экземпляре класса Library,
чтобы он стал доступен через шаблонный язык Django:
register.filter('cut', cut)
register.filter('lower', lower)
Метод filter() класса
Library принимает два аргумента:
Имя фильтра в виде строки;
Саму функцию фильтра.
Если вы используете Python версии 2.4 и выше, то вы можете
воспользоваться для этого декоратором
register.filter():
@register.filter(name='cut')
def cut(value, arg):
return value.replace(arg, '')
@register.filter
def lower(value):
return value.lower()
Если вы опустите аргумент name, как это сделано во втором примере, Django будет использовать имя функции в качестве имени фильтра.
Ниже приведён полный пример шаблонной библиотеки, предоставляющей фильтр cut:
from django import template
register = template.Library()
@register.filter(name='cut')
def cut(value, arg):
return value.replace(arg, '')
Теги более сложны в реализации, чем фильтры, потому что теги могут делать практически всё.
В главе «Шаблоны» было описано как работает шаблонная система, два шага: компиляция и рендеринг. Для того, чтобы определить свой шаблонный тег, вам понадобится указать Django как управлять этими шагами при обработке вашего тега.
При компиляции шаблона Django разбирает его текст на
узлы. Каждый узел является экземпляром
django.template.Node и имеет метод
render(). Следовательно, скомпилированный
шаблон является списком объектов Node.
Когда вы вызываете метод render() для
скомпилированного шаблона, сам шаблон вызывает метод
render() для каждого объекта
Node в своём списке узлов, передавая им
текущий контекст. Результаты рендеринга узлов будут объединены
вместе. Следовательно, для того, чтобы определить свой
шаблонный тег вы указываете метод преобразования тега в узел и
метод рендеринга для этого тега.
Далее в секциях мы рассмотрим все шаги необходимые для создания своего тега:
Для каждого встречающегося тега шаблонный парсер вызывает
функцию, передавая ей содержимое тега и ссылку на себя. Эта
функция отвечает за возвращение экземпляра класса
Node, построенного по переданной
информации.
Например, давайте реализуем шаблонный тег {% current_time %}, который отображает текущую дату и время, отформатированную в соответствии с параметром тега, используя синтаксис strftime (см. http://docs.python.org/library/datetime.html#datetime.date.strftime). Неплохо будет сначала продумать синтаксис тега. В нашем случае, пусть будет так:
<p>The time is {% current_time "%Y-%m-%d %I:%M %p" %}.</p>
Замечание
Конечно, этот шаблонный тег является лишним, стандартный тег Django {% now %} делает то же самое и с более простым синтаксисом. Данный шаблонный тег представлен здесь лишь в целях обучения.
Парсер должен выделять параметр и создавать объект
Node для этой функции:
from django import template
def do_current_time(parser, token):
try:
# метод split_contents() знает, что не надо разделять строки в кавычках
tag_name, format_string = token.split_contents()
except ValueError:
# камрады с мест сообщают, что в следующей строке должно использоваться
# token.split_contents()[0], требуется дополнительное подтверждение.
msg = '%r tag requires a single argument' % token.contents[0]
raise template.TemplateSyntaxError(msg)
return CurrentTimeNode(format_string[1:-1])
Разберём этот код:
Аргумент parser является экземпляром шаблонного парсера. В данном примере мы его не используем.
token.contents является строкой содержимого тега. В нашем примере это current_time "%Y-%m-%d %I:%M %p".
Метод token.split_contents() разделяет аргументы по пробелу, не разбивая строку в кавычках. Избегайте использовать token.contents.split(), которая использует стандартную семантику разбиения строк. Она не учитывает кавычки, разбивая все по пробелам.
Эта функция отвечает за вызов исключения django.template.TemplateSyntaxError с соответствующим сообщением на любую ошибку синтаксиса.
Не следует жёстко определять имя тега в сообщениях об ошибках. token.split_contents()[0] будет всегда именем вашего тега, даже если тег не имеет аргументов.
Функция возвращает экземпляр класса
CurrentTimeNode(который мы скоро определим), содержащий всё, что требуется узлу знать об этом теге. В данном случае, просто передается "%Y-%m-%d %I:%M %p". Ведущая и завершающая кавычки убираются с помощьюformat_string[1:-1].Функции компиляции шаблонных тегов должны возвращать подкласс
Node. Всё остальное — ошибка.
На втором шаге следует определить подкласс
Node, который должен иметь метод
render(). Продолжая наш пример, нам
надо определить CurrentTimeNode:
import datetime
class CurrentTimeNode(template.Node):
def __init__(self, format_string):
self.format_string = format_string
def render(self, context):
now = datetime.datetime.now()
return now.strftime(self.format_string)
Эти два метода (__init__ и
render) соответствую двум шагам
обработки (компиляция и рендеринг). Таким образом, метод
инициализации применяется только для сохранения строки
формата для дальнейшего использования, а метод
render() делает основную работу.
Аналогично шаблонным фильтрам, эти методы должны тихо игнорировать ошибки. Единственный момент, когда шаблонные теги могут вызвать исключение — момент компиляции.
В конце, следует зарегистрировать тег в экземпляре класса
Library. Регистрация собственных
тегов очень похожа на регистрацию своих фильтров. Создайте
экземпляр template.Library и вызовите метод
tag(). Например:
register.tag('current_time', do_current_time)
Метод tag() принимает два аргумента:
Имя шаблонного тега в виде строки. Если имя не указано, то используется имя функции компиляции.
Функция компиляции.
Как и при регистрации фильтра, также возможно использование декоратора (необходим Python версии 2.4+):
@register.tag(name="current_time")
def do_current_time(parser, token):
# ...
@register.tag
def shout(parser, token):
# ...
Если вы не укажете аргумент name, как было во втором примере, Django будет использовать имя функции в качестве имени тега.
Пример из предыдущей секции просто возвращал значение. Часто бывает полезным устанавливать шаблонную переменную вместо возвращения значения. Для этого авторы шаблонов могут использовать переменные, устанавливаемые их шаблонными тегами.
Для того, чтобы установить переменную в контексте,
используйте словарное назначение для контекстного объекта в
методе render(). Ниже показана
обновлённая версия класса
CurrentTimeNode:
class CurrentTimeNode2(template.Node):
def __init__(self, format_string):
self.format_string = format_string
def render(self, context):
now = datetime.datetime.now()
context['current_time'] = now.strftime(self.format_string)
return ''
Следует отметить, что метод render()
возвращает пустую строку. Данный метод должен всегда
возвращать строку. Раз шаблонный тег устанавливает
переменную, то метод должен возвращать пустую строку.
Вот так теперь это можно использовать:
{% current_time2 "%Y-%M-%d %I:%M %p" %}
<p>The time is {{ current_time }}.</p>
Но с классом CurrentTimeNode2 есть одна проблема — имя переменной
current_time жёстко определено. Это означает,
что вам придётся проверять ваши шаблоны, что они не
используют {{ current_time }}, так как
{% current_time2 %} слепо перепишет значение
этой переменной.
Более явное решение — позволить шаблонному тегу указывать имя устанавливаемой переменной, например так:
{% get_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):
now = datetime.datetime.now()
context[self.var_name] = 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:
msg = '%r tag requires arguments' % token.contents[0]
raise template.TemplateSyntaxError(msg)
m = re.search(r'(.*?) as (\w+)', arg)
if m:
fmt, var_name = m.groups()
else:
msg = '%r tag had invalid arguments' % tag_name
raise template.TemplateSyntaxError(msg)
if not (fmt[0] == fmt[-1] and fmt[0] in ('"', "'")):
msg = "%r tag's argument should be in quotes" % tag_name
raise template.TemplateSyntaxError(msg)
return CurrentTimeNode3(fmt[1:-1], var_name)
Теперь do_current_time() передаёт
строку формата и имя переменной в
CurrentTimeNode3.
Шаблонные теги могут работать как блоки, содержащие другие теги (типа {% if %}, {% for %} и так далее). Для создания такого шаблонного тега следует использовать 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 ''
Метод parse() получает кортеж имён
блочных тегов, до которых он должен вести обработку. Метод
возвращает экземпляр
django.template.NodeList, который является
списком всех объектов класса Node,
которые парсер обнаружил на обработки любого тега из
переданного кортежа.
Таким образом, в предыдущем примере nodelist является списком всех узлов между {% comment %} и {% endcomment %}.
После вызова метода parse() парсер ещё
не «использовал» тег {% endcomment %}, поэтому необходимо явно вызвать метод
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:
@register.tag
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(). Он просто вызывает метод
render() у каждого экземпляра класса
Node в списке.
Примеры более сложной обработки можно подсмотреть в исходном
коде {% if %}, {% for %},
{% ifequal %} и {% ifchanged %}. Их код можно найти в
django/template/defaulttags.py.
Множество шаблонных тегов принимают только один аргумент, строку или ссылку на шаблонную переменную, и возвращают строку после выполнения обработки, руководствуясь переданным аргументом и некоторой внешней информацией. Например, тег 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(token):
...
Про simple_tag следует сказать следующее:
Эта функция одного аргумента.
Проверку обязательного количества аргументов провела наша функция, так что не надо это здесь делать.
Кавычки вокруг аргументов (если есть) уже удалены, т.е., мы получаем чистую строку.
Другим общим типом тегов являются теги, которые отображают данные с помощью обработки другого шаблона. Например, интерфейс администратора Django использует свои шаблонные теги для отображения кнопок FIXME. Эти кнопки всегда выглядят одинаково, но их ссылки изменяются в соответствии с объектом, к которому они относятся. Данные кнопки являются отличным примером использования небольшого шаблона, который заполняется информацией из текущего объекта.
Этот тип тегов называется
включениями. Реализацию такого тега лучше демонстрировать на примере. Давайте создадим тег, который создаёт список значений, несколько из которых можно выбрать — класс Poll. Мы будем
использовать этот тег так:
{% show_results poll %}
Результат будет выглядеть так:
<ul>
<li>First choice</li>
<li>Second choice</li>
<li>Third choice</li>
</ul>
Сначала, мы определяем функцию, которая получает аргумент и возвращает словарь данных. Следует отметить, что нам требуется возвратить только словарь и ничего более сложного. Этот словарь будет использован как контекст:
def show_books_for_author(author):
books = author.book_set.all()
return {'books': books}
Затем мы создаём шаблон, который будет использоваться при обработке тега. Следуя нашему примеру, шаблон будет очень простым:
<ul>
{% for book in books %}
<li> {{ book }} </li>
{% endfor %}
</ul>
Наконец, мы создаём и регистрируем тег с помощью метода
inclusion_tag() объекта
Library.
Следуя нашему примеру, раз предыдущий шаблон был в файле
polls/result_snippet.html, регистрируем
тег так:
register.inclusion_tag('books/books_for_author.html')(show_books_for_author)
И как обычно, Python версии 2.4+ позволяет использование декоратора:
@register.inclusion_tag('books/books_for_author.html')
def show_books_for_author(show_books_for_author):
...
Иногда может потребоваться, чтобы ваш тег получал доступ к значениям контекста родительского шаблона. Для решения этой задачи Django предоставляет опцию takes_context. Если вы укажете её при создании шаблонного тега, то тегу не понадобятся обязательные аргументы, функция нижнего уровня будет использовать контекст шаблона из которого данный тег был вызван.
Например, вы реализовали тег включения, который всегда будет
использовать в контексте, содержащем переменные
home_link и home_title,
которые будут указывать на главную страницу. Функция может
быть такой:
@register.inclusion_tag('link.html', takes_context=True)
def jump_link(context):
return {
'link': context['home_link'],
'title': context['home_title'],
}
Замечание
Первый параметр функции должен называться context.
Шаблон link.html может содержать такую
строку:
Jump directly to <a href="{{ link }}">{{ title }}</a>.
Тогда, когда потребуется использовать этот тег, загрузите его библиотеку и вызовите его без аргументов:
{% jump_link %}
| Пред. | Уровень выше | След. |
| Загрузка шаблонов | Начало | Реализация своих шаблонных загрузчиков |
1 comment | Make a comment
Для получения наименования тега в блоках обработки ошибок вместо
msg = '%r tag requires a single argument' % token.contents[0]
необходимо писать
msg = '%r tag requires a single argument' % token.split_contents()[0]
т.к. token.contents[0] лишь указывает на первую букву наименования тэга
В оригинальном тексте именно так и написано