Django Book: использование системы шаблонов

Автор: | 13/02/2015
 

django_logo_2Предыдущая часть – Django Book: основы системы шаблонов.

Давайте рассмотрим систему шаблонов Django поближе, что вы могли лучше понять как она работает. Однако – мы пока не будем её внедрять в наши представления из первой главы. Наша цель сейчас – показать как эта система работает независимо от остальной части проекта на Django (другими словами – обычно вы будете использовать систему шаблонов вместе с представлениями Django, но мы хотели бы показать вам, что эта система – лишь одна из библиотек Django, и вы можете использовать её где и как угодно, а не только с представлениями).

Вот простое описание того, как вы можете использовать систему шаблонов в коде Python:

  1. Создаём объект Template, предоставляя исходный код шаблона как обычную строку;
  2. Вызываем метод render() объекта Template и определённым набором переменных (контекст, context), и получаем сформированный шаблон как строку, со всеми переменными и тегами шаблона, в соответствии с контекстом.

Примечание: тут и далее в роли консольного интерпретатора выступает IPython.

Вот как это может выглядеть в коде:

$ python manage.py shell

In [1]: from django import template

In [2]: t = template.Template('My name is {{ name }}.')

In [3]: c = template.Context({'name': 'Adrian'})

In [4]: print t.render(c)
My name is Adrian.

In [5]: c = template.Context({'name': 'Fred'})

In [6]: print t.render(c)
My name is Fred.

Ниже мы рассмотрим этот пример, описывая каждый шаг в деталях.

Создание объекта Template

Самый простой способ создания объекта Template – это напрямую вызвать его экземпляр. Класс Template находится в модуле django.template, а его конструктор принимает один аргумент – код самого шаблона. Давайте запустим интерпретатор, что бы посмотреть как весь этот код работает.

В директории, созданной командой django-admin.py startproject, которую мы выполняли в первой главе, введите команду python manage.py shell, что бы вызвать интерактивный интерпретатор.

Особенная консоль Python

Если вы раньше пользовались Python, вы можете удивиться – зачем нам запускать python manage.py shell вместо прямого вызова python? Обе команды откроют консоль интерпретатора, но python manage.py shell имеет ключевое отличие: перед запуском интерпретатора она укажет Django какой файл settings.py необходимо использовать. Многие системы Django, включая систему шаблонов, зависят от ваших настроек, и вы не сможете их использовать, пока фреймворк не будет знать их.

Если вам любопытно, как всё работает “за сценой”: Django ищет переменную окружения DJANGO_SETTINGS_MODULE, которая должна быть установлена для импорта пути вашего файла настроек settings.py. Например, значение этой переменной может выглядеть как ‘example.settings‘ , если ваш проект расположен в директории example.

Когда вы запускаете python manage.py shell – эта команда учитывает значение переменной DJANGO_SETTINGS_MODULE и загружает все необходимые настройки, и мы советуем вам использовать её в наших примерах, что бы уменьшить количвество необходимых действий для настройки вашего окружения.

Давайте рассмотрим некоторые основные элементы системы шаблонов:

In [7]: from django.template import Template

In [8]: t = Template('My name is {{ name }}.')

In [9]: print t

Если вы выполните этот код в консоли – вы должны увидеть что-то вроде такого:

<django.template.base.Template object at 0x2626190>

Значение 0x2626190 будет отличаться каждый раз, и оно не имеет особого значения (это “идентификатор” объекта Python).

Когда вы создаёте объект Template, система шаблонов компилирует исходный код шаблона во внешнюю оптимизированную форму данных, готовую к рендренингу. Однако, если ваш код содержит какие-то ошибки – то  выполнение Template() вызовет исключение TemplateSyntaxError:

In [10]: from django.template import Template

In [11]: t = Template('{% notatag %}')
---------------------------------------------------------------------------
TemplateSyntaxError                       Traceback (most recent call last)
<ipython-input-11-fdd5f59f5346> in <module>()
----> 1 t = Template('{% notatag %}')
...
TemplateSyntaxError: Invalid block tag: 'notatag'

Термин “блок тега” (block tag) тут относится к {% notatag %}, и является синонимом понятия “тег шаблона”.

Система вызывает TemplateSyntaxError  в таких случаях:

  • некорректный тег;
  • некорректный аргумент тега;
  • некорректные фильтры;
  • некорректные аргументы фильтров;
  • некорректный синтаксис шаблона;
  • незакрытые теги (для тегов, которые должны быть закрыты).

Template рендеринг

Как только вы создали объект Template, вы можете передать ему данные с помощью контекста. Контекст – простой набор имён переменных и связанных с ними значений. Шаблон использует их для наполнения его переменных и определения тегов.

Контекст предоставлен в Django классом Context из модуля django.template. Его конструктор принимает один опциональный аргумент – словарь с именами и значениями переменных. Что бы выполнить “сборку” объекта Template – используется метод render() с указанием контекста:

In [12]: from django.template import Context, Template

In [13]: t = Template('My name is {{ name }}.')

In [14]: c = Context({'name': 'Stephane'})

In [15]: t.render(c)
Out[15]: u'My name is Stephane.'

Обратите внимание, что выполнение t.render(c) возвращает объект Unicode, а не простую строку Python, что можно определить по префиксу uDjango использует Unicode объекты вместо стандартных строк во всем фреймворке. Если вы понимаете зачем это делается – то скажите спасибо Django за то, что он делает это всё сам. А если нет – то знайте, что Unicode в Django даёт возможность вашему приложению безболезненно обеспечивать поддержку огромного количества символов, кроме стандартных английских “A-z”.

Словари и контексты

Словари в Python связывают определённые ключи и значения переменных. Контексты в Django весьма схожи с ними, но имеют дополнительную функциональность, которую мы рассмотрим позже.

Имена переменных должны начинаться с буквы (A-Z или a-z), и могут содержать буквы, цифры, знаки подчёркивания и точки (хотя точки имеют особенное значение, которое мы рассмотри ниже в этой статье). Имена переменных чувствительны к регистру.

Вот пример компиляции и рендеринга с использованием шаблона, схожего с примером в начале главы:

In [1]: from django.template import Template, Context

In [2]: raw_template = """<p>Dear {{ person_name }},</p>
   ...: <p>Thanks for placing an order from {{ company }}. It's scheduled to
   ...: ship on {{ ship_date|date:"F j, Y" }}.</p>
   ...:
   ...: {% if ordered_warranty %}
   ...: <p>Your warranty information will be included in the packaging.</p>
   ...: {% else %}
   ...: <p>You didn't order a warranty, so you're on your own when
   ...: the products inevitably stop working.</p>
   ...: {% endif %}
   ...: <p>Sincerely,<br />{{ company }}</p>"""

In [3]: t = Template(raw_template)

In [4]: import datetime

In [5]: c = Context({'person_name': 'John Smith',
   ...: 'company': 'Outdoor Equipment',
   ...: 'ship_date': datetime.date(2009, 4, 2),
   ...: 'ordered_warranty': False})

In [6]: t.render(c)
Out[6]: u"<p>Dear John Smith,</p>n<p>Thanks for placing an order from Outdoor Equipment. It's scheduled tonship on April 2, 2009.</p>nn<p>You didn't order a warranty, so you're on your own whennthe products inevitably stop working.</p>nn<p>Sincerely,<br />Outdoor Equipment</p>"

Давайте рассмотрим этот код подробнее:

  • Сначала, мы импортируем классы Template и Context из модуля django.template.
  • Затем, мы сохраняем исходный код нашего шаблона в переменной raw_template. Обратите внимание, что мы используем тройные кавычки для определения строки, так как они позволяют использовать несколько строк, тогда как обычные одинарные или двойные кавычки – могут использоваться только для строки в одну линию.

  • Далее, мы создаём объект шаблона с именем t, передавая raw_template конструктору класса Template.

  • Мы импортируем модуль datetime из стандартной библиотеки Python, так как он нам потребуется в следующем выражении.

  • Затем, мы создаём объект Context с именем c. Конструктор класса Context в качестве аргумента принимает словарь Python, который связывает имена переменных с их значениями. Тут например, мы указываем, что переменная person_name имеет значение ‘John Smith’, переменная company – ‘Outdoor Equipment‘ и так далее.

  • В конце – мы вызываем метод render() нашего объекта шаблона, передавая ему аргументом наш контекст, а он нам возвращает сформированный шаблон, т.е. – с добавленными переменными и обработанными тегами.

    Заметьте, что параграф “You didn’t order a warranty” отображается, так как переменная ordered_warranty = False. Так же, обратите внимание на дату April 2, 2009, которая отображается в соответствии с заданным фильтром ‘F j, Y‘ (мы рассмотрим форматирование строки для фильтра date чуть позже).

    Если вы новичок в Python, вы можете удивиться – почему в выводе присутствует символ новой строки ‘n‘, а не выполняется её перенос? Это связанно с особенностью интерпретатора Python – вызов t.render(c) возвращает строку, а по умолчанию интерактивный интерпретатор отображает представление строки, а не печатает её реальное значение. Если вы хотите увидеть действительное значение значение строки, с использованием символов переноса – используйте оператор print: print t.render(c):

In [7]: print(t.render(c))
<p>Dear John Smith,</p>
<p>Thanks for placing an order from Outdoor Equipment. It's scheduled to
ship on April 2, 2009.</p>

<p>You didn't order a warranty, so you're on your own when
the products inevitably stop working.</p>

<p>Sincerely,<br />Outdoor Equipment</p>

Несколько контекстов, один Template

Когда у вас уже есть объект Template – вы можете использовать несколько различных контекстов с ним.

Например:

In [8]: from django.template import Template, Context

In [9]: t = Template('Hello, {{ name }}')


In [10]:

In [10]: print t.render(Context({'name': 'John'}))
Hello, John

In [11]: print t.render(Context({'name': 'Julie'}))
Hello, Julie

In [12]: print t.render(Context({'name': 'Pat'}))
Hello, Pat

При использовании одного шаблона, как в примере выше, более эффективно будет создать объект шаблона, а затем вызывать render() несколько раз, по мере необходимости:

# Плохой пример
for name in ('John', 'Julie', 'Pat'):
    t = Template('Hello, {{ name }}')
    print t.render(Context({'name': name}))

# Хороший пример
t = Template('Hello, {{ name }}')
for name in ('John', 'Julie', 'Pat'):
    print t.render(Context({'name': name}))

Разбор шаблона в Django выполняется очень быстро, и большая часть реализована через вызов только одного регулярного выражения, в отличии от шаблонов, основанных на XML, которые взывают XML-парсер и могут выполняться на несколько порядков медленее, чем это реализовано в Django.

Поиск переменных в контексте

До сих пор в примерах мы передавали простые значения в контексты – в основном это были строки и datetime.date. Однако, система шаблонов очень элегантно обрабатывает более сложные структуры, такие как списки, словари и другие объекты.

Ключ для передачи сложных структур данных в шаблон Django – это точка. Используйте её, что бы получить доступ к ключам словарей, атрибутам, методам или индексам объектов.

Давайте продемонстрируем это на примерах. Например, вы хотите передать словарь Python в шаблон. Для получения доступа к ключам словаря – используем точку:

In [13]: from django.template import Template, Context

In [14]: person = {'name': 'Sally', 'age': '43'}

In [15]: t = Template('{{ person.name }} is {{ person.age }} years old.')

In [16]: c = Context({'person': person})

In [17]: print(t.render(c))
Sally is 43 years old.

Так же, точка даёт доступ к атрибутам объекта. Например, объект datetime.date имеет атрибуты year, month и day. Вы можете использовать точку, что бы получить к ним доступ из шаблона Django:

In [18]: from django.template import Template, Context

In [19]: import datetime

In [20]: d = datetime.date(1993, 5, 2)

In [21]: d.year
Out[21]: 1993

In [22]: d.month
Out[22]: 5

In [23]: d.day
Out[23]: 2

In [24]: t = Template('The month is {{ date.month }} and the year is {{ date.year }}.')

In [25]: c = Context({'date': d})

In [26]: print(t.render(c))
The month is 5 and the year is 1993.

В следующем примере мы используем свой класс и  продемонстрируем как с помощью точки в переменной получить доступ к атрибутам произвольных объектов:

In [27]: from django.template import Template, Context

In [28]: class Person(object):
   ....:     def __init__(self, first_name, last_name):
   ....:         self.first_name, self.last_name = first_name, last_name
   ....:

In [29]: t = Template('Hello, {{ person.first_name }} {{ person.last_name }}.')

In [30]: c = Context({'person': Person('John', 'Smith')})

In [31]: print(t.render(c))
Hello, John Smith.

Точки так же можно использовать для ссылок на методы объектов классов. Например, каждая строка в Python имеет методы upper(), isdigit() и другие строковые методы. Вы можете вызвать их в Django с помощью точек:

In [32]: from django.template import Template, Context

In [33]: t = Template('{{ var }} -- {{ var.upper }} -- {{ var.isdigit }}')

In [34]: print(t.render(Context({'var': 'hello'})))
hello -- HELLO -- False

In [35]: print(t.render(Context({'var': '123'})))
123 -- 123 -- True

Заметьте, что тут не используются скобки при вызове метода объекта. Кроме того, методам нельзя передать аргументы – вы можете использовать только те методы, которые не требуют аргументов (мы рассмотрим эти моменты далее в этой главе).

И напоследок – точки так же используются для доступа к индексам списков:

In [36]: from django.template import Template, Context

In [37]: t = Template('Item 2 is {{ items.2 }}.')

In [38]: c = Context({'items': ['apples', 'bananas', 'carrots']})

In [39]: print(t.render(c))
Item 2 is carrots.

Нельзя использовать отрицательные индексы списков. Например, переменная шаблона {{ items.-1 }} вызовет исключение TemplateSyntaxError.

Списки Python

Напомним, что списки Python используют “нулевой индекс” – первый элемент списка всегда равен 0, второй – 1, и так далее.

Подводя итоги: когда шаблон встречает точку в имени переменной – он выполняет поиск в таком порядке:

  • Поиск словаря (например – foo["bar"])
  • Поиск атрибута (например –  foo.bar)
  • Вызов метода (например – foo.bar())
  • Поиск индекса списка (например – foo[2])

Будет использован первый тип, который сработает.

Поиск решения для точки может быть включен в несколько уровней. Например, в коде ниже используется {{ person.name.upper }}, который будет переведён в поиск словаря (person['name']) , а затем вызова метода (upper()):

In [40]: from django.template import Template, Context

In [41]: person = {'name': 'Sally', 'age': '43'}

In [42]: t = Template('{{ person.name.upper }} is {{ person.age }} years old.')

In [43]: c = Context({'person': person})

In [44]: print(t.render(c))
SALLY is 43 years old.

Поведение вызова метода

Вызовы методов немного более сложные, чем другие. Вот некоторые моменты, которые стоит знать:

  • Если во время поиска метода он вернёт исключение – оно будет будет использовано как ошибка, если для исключения не установлен атрибут silent_variable_failure. Если же для исключения silent_variable_failure = True – переменная создаст пустую строку, например:

    >>> t = Template("My name is {{ person.first_name }}.")
    >>> class PersonClass3:
    ...     def first_name(self):
    ...         raise AssertionError, "foo"
    >>> p = PersonClass3()
    >>> t.render(Context({"person": p}))
    Traceback (most recent call last):
    ...
    AssertionError: foo
    
    >>> class SilentAssertionError(AssertionError):
    ...     silent_variable_failure = True
    >>> class PersonClass4:
    ...     def first_name(self):
    ...         raise SilentAssertionError
    >>> p = PersonClass4()
    >>> t.render(Context({"person": p}))
    u'My name is .'
    
  • Вызов метода сработает только в том случае, если метод не требует аргументов. В противном случае – система перейдёт к следующему типу (поиск индекса).

  • Конечно, некоторые методы имеют “побочные эффекты”, и будет как минимум глупо, и даже, возможно, небезопасно, предоставлять системе шаблонов к ним доступ.

    Например, у вас есть экземпляр BankAccount, у которого есть метод delete(). Если шаблон включает в себя что-то вроде {{ account.delete }}, где account – это объект BankAccount, то он будет удалён во врем создания шаблона.

    Что бы избежать этого – установите атрибут функции alters_data для метода:

    def delete(self):
        # Delete the account
    delete.alters_data = True

    Система шаблона не будет его выполнять. Продолжая пример выше – если шаблон включает в себя {{ account.delete }}, а метод delete()  имеет атрибут alters_data = True, тогда метод delete() не будет выполнен во время рендеринга шаблона.

Обработка неверных переменных

По умолчанию, если переменная не существует, система шаблонов создаст пустую строку, например:

In [49]: from django.template import Template, Context

In [50]: t = Template('Your name is {{ name }}.')

In [51]: print(t.render(Context()))
Your name is .

In [52]: print(t.render(Context({'var': 'hello'})))
Your name is .

In [53]: print(t.render(Context({'NAME': 'hello'})))
Your name is .

In [54]: print(t.render(Context({'Name': 'hello'})))
Your name is .

Система обработает ошибку без уведомлений, так как она должна быть относительно терпимой к обычным человеческим ошибкам. В этом случае, все поиски были ошибочны, так как имена переменных были неверными. В реальном мире  недопустимо, что бы сайт стал недоступен из-за маленькой ошибки в шаблоне.

Объекты контекстов

В большинстве случаев – вы будете инициализировать объект Context, передавая ему заполненный словарь. Но вы так же моете добавлять или удалять элементы из контекста, как только он создан, используя стандартный синтаксис Python:

In [55]: from django.template import Context

In [56]: c = Context({"foo": "bar"})

In [57]: c['foo']
Out[57]: 'bar'

In [58]: del c['foo']

In [59]: c['foo']
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-59-46058764a7fc> in <module>()
----> 1 c['foo']

/usr/lib/python2.6/site-packages/django/template/context.pyc in __getitem__(self, key)
     53             if key in d:
     54                 return d[key]
---> 55         raise KeyError(key)
     56
     57     def __delitem__(self, key):

KeyError: 'foo'

In [60]: c['newvariable'] = 'hello'

In [61]: c['newvariable']
Out[61]: 'hello'

Продолжение – Django Book: основные теги и фильтры шаблона.