Пользовательские теги и фильтры шаблонов

Язык шаблонов 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 чтобы тег работал. Это функция безопасности: она позволяет размещать код Python из многих библиотек шаблонов на одном компьютере без необходимости разрешать доступ ко всем этим библиотекам для каждой установки Django.{% load %}

Нет ограничений на количество модулей, которые могут быть помещены в пакет templatetags . Просто имейте в виду, что команда загрузит теги / фильтры указанного имени модуля Python, а не имени приложения.{% load %}

Чтобы быть действительным, модуль библиотеки тегов должен содержать именованную переменную уровня модуля, 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() принимает два параметра:

  1. Название фильтра, строка.
  2. Функция компилятора, функция 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.
        ...
    

Код шаблона шаблона попадает в одну из следующих двух ситуаций:

  1. Ваш фильтр не вносит новый 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 будет экранировать содержимое всякий раз, когда начальная переменная d еще не помечена как «безопасная».

    По умолчанию is_safe есть False , и вы можете просто опустить его, когда он не нужен.

    Будьте осторожны, решая, действительно ли ваш фильтр сохраняет безопасные строки. Если вы удалите символы, вы можете непреднамеренно оставить в выводе несбалансированные HTML-теги или объекты. Например, удаление одного > из исходных материалов может превратить его <a> в содержимое, <a которое затем следует убрать с экрана, чтобы избежать проблем. Точно так же удаление точки с запятой ( ; ) может преобразоваться &amp; в &amp , который больше не является допустимым объектом и, следовательно, должен быть снова экранирован. В большинстве случаев это будет не так сложно, но помните о потенциальной проблеме такого рода при проверке кода.

    Пометка фильтра с помощью is_safe принудительно возвращает значение фильтра в виде строки. Если ваш фильтр должен возвращать логическое или какое-либо другое нетекстовое значение, маркировка с помощью is_safe , вероятно, приведет к непредвиденным последствиям (например, преобразование логического значения False в строку «False»).

  2. Другой возможный вариант - позаботиться о любом экранировании вручную в коде фильтра. Это необходимо, когда вы вводите новые 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 (если нет, мы используем нейтральную функцию как 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 ''

Когда этот флаг установлен, и если первый параметр фильтра является чувствительным к часовому поясу объектом даты / времени, 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.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>

Теперь создайте и зарегистрируйте тег include, вызвав метод inclusion_tag() объекта Library . Следуя нашему примеру, если указанный выше шаблон находится в файле с именем results.html в каталоге, просматриваемом загрузчиком шаблонов, мы регистрируем тег следующим образом:

# Here, register is a django.template.Library instance, as before
@register.inclusion_tag('results.html')
def show_results(poll):
    ...

Также можно зарегистрировать тег include, используя экземпляр 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() если это необходимо. Когда переменная наконец отобразится, на нее будет влиять настройка автоматического перехода, действующая в то время, поэтому содержимое, которое больше не должно экранироваться, должно быть отмечено как таковое.

Аналогичным образом, если тег шаблона создает новый контекст для выполнения некоторой суб-рендеринга, установите для атрибута auto-escape значение текущего контекста. Метод __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. Поток 1 выполняет свою первую итерацию цикла, CycleNode.render() возвращает "row1"
  2. Поток 2 выполняет свою первую итерацию цикла, CycleNode.render() возвращает "row2"
  3. Поток 1 выполняет вторую итерацию цикла, CycleNode.render() возвращает "row1".
  4. Поток 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() принимает два параметра:

  1. Имя тега шаблона (строка). Если не указано, будет использовано имя функции компиляции.
  2. Функция компилятора, функция 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() вернет три значения:

  1. Имя тега
  2. Строка 'blog_entry.date_updated' (без кавычек).
  3. Строка форматирования . Возвращаемое значение будет содержать открывающую и закрывающую кавычки для таких постоянных строк.'"%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>

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

Любая переменная, определенная в контексте, будет доступна только в том же «блоке» шаблона, в котором она была определена. Такое поведение предназначено; он определяет область действия переменных, чтобы они не конфликтовали с контекстом других блоков.

Но все еще есть проблема 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

Copyright ©2021 All rights reserved