Spec-Zone.ru › Django 2.0

Шаблоны

В качестве веб-фреймворка Django нуждается в удобном способе динамического генерирования HTML. Наиболее распространённый подход использует шаблоны. Шаблон содержит статические части желаемого HTML-вывода, а также специальный синтаксис, описывающий, как будет вставлено динамическое содержимое. Для практического примера создания HTML-страниц с помощью шаблонов см. Урок 3.

Проект Django может быть настроен с одним или несколькими движками шаблонов (или даже с нулём, если вы не используете шаблоны). Django поставляется с встроенными бэкендами для собственной системы шаблонов, творчески названной языком шаблонов Django (DTL), и для популярной альтернативы Jinja2. Бэкэнды для других языков шаблонов могут быть доступны от сторонних разработчиков.

Django определяет стандартный API для загрузки и рендеринга шаблонов независимо от бэкенда. Загрузка заключается в поиске шаблона по заданному идентификатору и его предварительной обработке, обычно компиляции его в представление в оперативной памяти. Рендеринг означает интерполяцию шаблона с данными контекста и возвращение результирующей строки.

Язык шаблонов Django — это собственная система шаблонов Django. До версии Django 1.8 это был единственный доступный встроенный вариант. Это хорошая библиотека шаблонов, хотя она довольно категорична и обладает некоторыми особенностями. Если у вас нет веской причины выбрать другой бэкенд, вы должны использовать DTL, особенно если вы пишете подключаемый модуль и планируете распространять шаблоны. Приложения Django contrib, которые включают шаблоны, такие как django.contrib.admin, используют DTL.

По историческим причинам как общая поддержка движков шаблонов, так и реализация языка шаблонов Django находятся в пространстве имён django.template.

Предупреждение

Система шаблонов не защищена от ненадежных авторов шаблонов. Например, сайт не должен разрешать своим пользователям предоставлять собственные шаблоны, так как авторы шаблонов могут совершать такие действия, как атаки XSS и доступ к свойствам переменных шаблонов, которые могут содержать конфиденциальную информацию.

Поддержка движков шаблонов

Настройка

Движки шаблонов настраиваются с помощью настройки TEMPLATES. Это список конфигураций, по одной для каждого движка. Значение по умолчанию — пустое. Значение, сгенерированное командой startproject, более полезно:

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            # ... some options here ...
        },
    },
]

BACKEND — это путь к классу движка шаблонов в Python, реализующему API бэкенда Django для шаблонов. Встроенные бэкэнды — django.template.backends.django.DjangoTemplates и django.template.backends.jinja2.Jinja2.

Поскольку большинство движков загружают шаблоны из файлов, конфигурация верхнего уровня для каждого движка содержит две общие настройки:

  • DIRS определяет список каталогов, в которых движок должен искать исходные файлы шаблонов в порядке поиска.
  • APP_DIRS указывает, должен ли движок искать шаблоны внутри установленных приложений. Каждый бэкенд определяет стандартное имя подкаталога внутри приложений, где должны храниться его шаблоны.

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

OPTIONS содержит настройки, специфичные для бэкенда.

Использование

Модуль django.template.loader определяет две функции для загрузки шаблонов.

get_template(template_name, using=None) [source]

Эта функция загружает шаблон с заданным именем и возвращает объект Template.

Точный тип возвращаемого значения зависит от бэкенда, который загрузил шаблон. Каждый бэкенд имеет свой собственный класс Template.

get_template() пытается использовать каждый движок шаблонов в порядке, пока не найдёт успешный. Если шаблон не найден, он вызывает TemplateDoesNotExist. Если шаблон найден, но содержит недопустимый синтаксис, он вызывает TemplateSyntaxError.

Как шаблоны ищутся и загружаются, зависит от бэкенда каждого движка и его конфигурации.

Если вы хотите ограничить поиск определённым движком шаблонов, передайте имя бэкенда NAME в аргументе using.

select_template(template_name_list, using=None) [source]

select_template() аналогична get_template(), за исключением того, что она принимает список имён шаблонов. Она пытается использовать каждое имя в порядке и возвращает первый найденный шаблон.

Если загрузка шаблона завершается неудачей, могут быть вызваны следующие два исключения, определённые в django.template.

exception TemplateDoesNotExist(msg, tried=None, backend=None, chain=None) [source]

Это исключение вызывается, когда шаблон не найден. Оно принимает следующие необязательные аргументы для заполнения разбора шаблона на странице отладки:

backend
Экземпляр бэкенда шаблона, из которого произошло исключение.
tried
Список источников, которые были проверены при поиске шаблона. Он отформатирован как список кортежей, содержащих (origin, status), где origin — объект типа «источник», а status — строка с причиной, по которой шаблон не был найден.
chain
Список промежуточных исключений TemplateDoesNotExist, возникших при попытке загрузить шаблон. Это используется такими функциями, как get_template(), которые пытаются загрузить данный шаблон из нескольких движков.
exception TemplateSyntaxError(msg) [source]

Это исключение вызывается, когда шаблон был найден, но содержит ошибки.

Объекты Template, возвращаемые get_template() и select_template(), должны предоставлять метод render(), с указанной сигнатурой:

Template.render(context=None, request=None)

Рендерит данный шаблон с заданным контекстом.

Если context предоставлен, он должен быть dict. Если он не предоставлен, движок рендерит шаблон с пустым контекстом.

Если request предоставлен, он должен быть HttpRequest. Затем движок должен сделать его, а также токен CSRF, доступным в шаблоне. Способ, которым это достигается, зависит от каждого бэкенда.

Вот пример алгоритма поиска. Для этого примера настройка TEMPLATES выглядит так:

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [
            '/home/html/example.com',
            '/home/html/default',
        ],
    },
    {
        'BACKEND': 'django.template.backends.jinja2.Jinja2',
        'DIRS': [
            '/home/html/jinja2',
        ],
    },
]

Если вы вызовете get_template('story_detail.html'), Django будет искать следующие файлы в указанном порядке:

  • /home/html/example.com/story_detail.html ('django' движок)
  • /home/html/default/story_detail.html ('django' движок)
  • /home/html/jinja2/story_detail.html ('jinja2' движок)

Если вы вызовете select_template(['story_253_detail.html', 'story_detail.html']), Django будет искать:

  • /home/html/example.com/story_253_detail.html ('django' движок)
  • /home/html/default/story_253_detail.html ('django' движок)
  • /home/html/jinja2/story_253_detail.html ('jinja2' движок)
  • /home/html/example.com/story_detail.html ('django' движок)
  • /home/html/default/story_detail.html ('django' движок)
  • /home/html/jinja2/story_detail.html ('jinja2' движок)

Когда Django найдёт существующий шаблон, он прекратит поиск.

Подсказка

Вы можете использовать select_template() для гибкой загрузки шаблонов. Например, если вы написали новостную статью и хотите, чтобы некоторые статьи имели пользовательские шаблоны, используйте что-то вроде select_template(['story_%s_detail.html' % story.id, 'story_detail.html']). Это позволит вам использовать пользовательский шаблон для отдельной статьи с резервным шаблоном для статей, у которых нет пользовательских шаблонов.

Возможна — и предпочтительна — организация шаблонов в подкаталогах внутри каждого каталога, содержащего шаблоны. Принято создавать подкаталог для каждого приложения Django с подкаталогами внутри этих подкаталогов по мере необходимости.

Сделайте это для собственного удобства. Хранение всех шаблонов в корневом уровне одного каталога становится неудобным.

Для загрузки шаблона, который находится в подкаталоге, просто используйте слеш, как в этом примере:

get_template('news/story_detail.html')

Используя ту же настройку TEMPLATES, как и выше, это попытается загрузить следующие шаблоны:

  • /home/html/example.com/news/story_detail.html ('django' движок)
  • /home/html/default/news/story_detail.html ('django' движок)
  • /home/html/jinja2/news/story_detail.html ('jinja2' движок)

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

render_to_string(template_name, context=None, request=None, using=None) [source]

render_to_string() загружает шаблон, как get_template(), и сразу вызывает его метод render(). Он принимает следующие аргументы.

template_name
Имя шаблона для загрузки и рендеринга. Если это список имён шаблонов, Django использует select_template() вместо get_template() для поиска шаблона.
context
Словарь (dict) для использования в качестве контекста шаблона при рендеринге.
request
Необязательный объект HttpRequest, который будет доступен во время рендеринга шаблона.
using
Необязательный движок шаблонов NAME. Поиск шаблона будет ограничен этим движком.

Пример использования:

from django.template.loader import render_to_string
rendered = render_to_string('my_template.html', {'foo': 'bar'})

См. также короткую запись render(), которая вызывает render_to_string() и передаёт результат в HttpResponse, пригодный для возврата из представления.

Наконец, вы можете использовать настроенные движки напрямую:

engines

Движки шаблонов доступны в django.template.engines:

from django.template import engines

django_engine = engines['django']
template = django_engine.from_string("Hello {{ name }}!")

Ключ поиска — 'django' в этом примере — это NAME движка.

Встроенные движки

class DjangoTemplates [source]

Установите BACKEND в 'django.template.backends.django.DjangoTemplates' для настройки движка Django шаблонов.

Когда APP_DIRS равно True, движки DjangoTemplates ищут шаблоны в подкаталоге templates установленных приложений. Это общее имя сохранено для обратной совместимости.

Движки DjangoTemplates принимают следующие OPTIONS:

  • 'autoescape': логическое значение, которое управляет включением автоэскейпинга HTML.

    По умолчанию True.

    Предупреждение

    Установите это значение только в False случае рендеринга не HTML-шаблонов!

  • 'context_processors': список пути Python в вызываемые функции, которые используются для заполнения контекста при рендеринге шаблона с запросом. Эти вызываемые функции принимают объект запроса в качестве аргумента и возвращают словарь (dict) элементов, которые будут объединены в контекст.

    По умолчанию пустой список.

    См. RequestContext для получения дополнительной информации.

  • 'debug': логическое значение, которое включает/выключает отладочный режим шаблонов. Если это True, на странице с ошибкой отобразится подробный отчёт обо всех исключениях, возникших при рендеринге шаблона. Этот отчёт содержит соответствующий фрагмент шаблона с выделенной строкой.

    По умолчанию, значение параметра DEBUG.

  • 'loaders': список путей Python к классам загрузчиков шаблонов. Каждый класс Loader знает, как импортировать шаблоны из определённого источника. Необязательно, вместо строки может использоваться кортеж. Первый элемент кортежа — имя класса Loader, а последующие элементы передаются в Loader при инициализации.

    Значение по умолчанию зависит от значений DIRS и APP_DIRS.

    Подробности см. в разделе Типы загрузчиков.

  • 'string_if_invalid': строковый вывод, который система шаблонов должна использовать для недопустимых (например, неправильно написанных) переменных.

    По умолчанию пустая строка.

    Подробности см. в разделе Как обрабатываются недопустимые переменные шаблонов.

  • 'file_charset': кодировка, используемая для чтения файлов шаблонов на диске.

    По умолчанию, значение FILE_CHARSET.

  • 'libraries': словарь меток и путей Python к модулям тегов шаблонов для регистрации в движке шаблонов. Это позволяет добавлять новые библиотеки или предоставлять альтернативные метки для существующих. Например:

    OPTIONS={
        'libraries': {
            'myapp_tags': 'path.to.myapp.tags',
            'admin.urls': 'django.contrib.admin.templatetags.admin_urls',
        },
    }
    

    Библиотеки могут быть загружены, передав соответствующий ключ словаря тегу {% load %}.

  • 'builtins': список путей Python к модулям тегов шаблонов для добавления в встроенные. Например:

    OPTIONS={
        'builtins': ['myapp.builtins'],
    }
    

    Теги и фильтры из встроенных библиотек можно использовать без предварительного вызова тега {% load %}.

class Jinja2 [source]

Требуется установка Jinja2:

$ pip install Jinja2

Установите BACKEND в 'django.template.backends.jinja2.Jinja2' для настройки движка Jinja2.

Когда APP_DIRS равно True, движки Jinja2 ищут шаблоны в подкаталоге jinja2 установленных приложений.

Наиболее важным элементом в OPTIONS является 'environment'. Это путь Python к вызываемой функции, возвращающей среду Jinja2. По умолчанию 'jinja2.Environment'. Django вызывает эту функцию и передает другие параметры в качестве ключевых аргументов. Кроме того, Django добавляет значения по умолчанию, которые отличаются от значений по умолчанию Jinja2 для некоторых параметров:

  • 'autoescape': True
  • 'loader': загрузчик, настроенный для DIRS и APP_DIRS
  • 'auto_reload': settings.DEBUG
  • 'undefined': DebugUndefined if settings.DEBUG else Undefined

Движки Jinja2 также принимают следующие OPTIONS:

  • 'context_processors': список путей Python к вызываемым функциям, которые используются для заполнения контекста при рендеринге шаблона с запросом. Эти вызываемые функции принимают объект запроса в качестве аргумента и возвращают словарь (dict) элементов, которые будут объединены в контекст.

    По умолчанию пустой список.

    Использование обработчиков контекста с шаблонами Jinja2 не рекомендуется.

    Обработчики контекста полезны для Django шаблонов, потому что Django шаблоны не поддерживают вызов функций с аргументами. Поскольку Jinja2 не имеет этого ограничения, рекомендуется помещать функцию, которую вы бы использовали в качестве обработчика контекста, в глобальные переменные, доступные для шаблона, используя jinja2.Environment, как описано ниже. Затем вы можете вызвать эту функцию в шаблоне:

    {{ function(request) }}
    

    Некоторые обработчики контекста Django шаблонов возвращают фиксированное значение. Для шаблонов Jinja2 такой уровень косвенности не нужен, так как вы можете добавить константы непосредственно в jinja2.Environment.

    Оригинальный случай использования добавления обработчиков контекста для Jinja2 включал:

    • Произведение дорогостоящих вычислений, зависящих от запроса.
    • Необходимость результата во всех шаблонах.
    • Использование результата несколько раз в каждом шаблоне.

    Если не все эти условия выполнены, передача функции в шаблон проще и более соответствует дизайну Jinja2.

Добавлено в Django 1.11:

Был добавлен параметр 'context_processors'.

Значение по умолчанию намеренно минимально. Если шаблон отображается с запросом (например, при использовании render()), бэкенд Jinja2 добавляет в контекст переменные request, csrf_input, и csrf_token. Помимо этого, бэкенд не создаёт среду с функциями Django. Он не знает о фильтрах и тегах Django. Для использования специфичных для Django API необходимо настроить их в среде.

Например, вы можете создать myproject/jinja2.py с этим содержимым:

from django.contrib.staticfiles.storage import staticfiles_storage
from django.urls import reverse

from jinja2 import Environment


def environment(**options):
    env = Environment(**options)
    env.globals.update({
        'static': staticfiles_storage.url,
        'url': reverse,
    })
    return env

и установить опцию 'environment' в значение 'myproject.jinja2.environment'.

Затем вы можете использовать следующие конструкции в шаблонах Jinja2:

<img src="{{ static('path/to/company-logo.png') }}" alt="Company Logo">

<a href="{{ url('admin:index') }}">Administration</a>

Концепции тегов и фильтров существуют как в языке шаблонов Django, так и в Jinja2, но используются по-разному. Поскольку Jinja2 поддерживает передачу аргументов вызываемым объектам в шаблонах, многие функции, требующие тега или фильтра в шаблонах Django, могут быть реализованы простым вызовом функции в шаблонах Jinja2, как показано в примере выше. Глобальное пространство имён Jinja2 исключает необходимость обработчиков контекста шаблонов. Язык шаблонов Django не имеет эквивалента тестов Jinja2.

Настраиваемые бэкенды

Вот как реализовать настраиваемый бэкенд шаблонов для использования другой системы шаблонов. Бэкенд шаблонов — это класс, который наследует django.template.backends.base.BaseEngine. Он должен реализовать get_template() и необязательно from_string(). Вот пример для вымышленной библиотеки шаблонов foobar.

from django.template import TemplateDoesNotExist, TemplateSyntaxError
from django.template.backends.base import BaseEngine
from django.template.backends.utils import csrf_input_lazy, csrf_token_lazy

import foobar


class FooBar(BaseEngine):

    # Name of the subdirectory containing the templates for this engine
    # inside an installed application.
    app_dirname = 'foobar'

    def __init__(self, params):
        params = params.copy()
        options = params.pop('OPTIONS').copy()
        super().__init__(params)

        self.engine = foobar.Engine(**options)

    def from_string(self, template_code):
        try:
          return Template(self.engine.from_string(template_code))
        except foobar.TemplateCompilationFailed as exc:
            raise TemplateSyntaxError(exc.args)

    def get_template(self, template_name):
        try:
            return Template(self.engine.get_template(template_name))
        except foobar.TemplateNotFound as exc:
            raise TemplateDoesNotExist(exc.args, backend=self)
        except foobar.TemplateCompilationFailed as exc:
            raise TemplateSyntaxError(exc.args)


class Template:

    def __init__(self, template):
        self.template = template

    def render(self, context=None, request=None):
        if context is None:
            context = {}
        if request is not None:
            context['request'] = request
            context['csrf_input'] = csrf_input_lazy(request)
            context['csrf_token'] = csrf_token_lazy(request)
        return self.template.render(context)

Дополнительную информацию см. в DEP 182.

Интеграция отладки для настраиваемых движков

Страница отладки Django имеет крючки для предоставления подробной информации при возникновении ошибки шаблона. Настраиваемые движки шаблонов могут использовать эти крючки для улучшения информации об отслеживании, отображаемой пользователям. Доступны следующие крючки:

Постмортам шаблонов

Постмортам появляется, когда TemplateDoesNotExist генерируется исключение. Он перечисляет движки и загрузчики шаблонов, которые использовались при попытке найти заданный шаблон. Например, если настроены два движка Django, постмортам будет выглядеть так:

../_images/postmortem.pngTemplateDoesNotExist. Бэкэнды, использующие посмертный анализ, должны указать происхождение шаблона в объекте шаблона.

Информация о контекстных строках

Если при разборе или рендеринге шаблона возникает ошибка, Django может отобразить строку, на которой произошла ошибка. Например:

../_images/template-lines.pngdict со следующими значениями:
  • 'name': Название шаблона, в котором произошла ошибка.
  • 'message': Сообщение об ошибке.
  • 'source_lines': Строки до, после и включая строку, на которой произошла ошибка. Это контекст, поэтому он не должен содержать более 20 строк.
  • 'line': Номер строки, на которой произошла ошибка.
  • 'before': Содержимое строки с ошибкой перед токеном, вызвавшим ошибку.
  • 'during': Токен, вызвавший ошибку.
  • 'after': Содержимое строки с ошибкой после токена, вызвавшего ошибку.
  • 'total': Количество строк в source_lines.
  • 'top': Номер строки, где начинается source_lines.
  • 'bottom': Номер строки, где заканчивается source_lines.

Учитывая ошибку шаблона выше, template_debug будет выглядеть так:

{
    'name': '/path/to/template.html',
    'message': "Invalid block tag: 'syntax'",
    'source_lines': [
        (1, 'some\n'),
        (2, 'lines\n'),
        (3, 'before\n'),
        (4, 'Hello {% syntax error %} {{ world }}\n'),
        (5, 'some\n'),
        (6, 'lines\n'),
        (7, 'after\n'),
        (8, ''),
    ],
    'line': 4,
    'before': 'Hello ',
    'during': '{% syntax error %}',
    'after': ' {{ world }}\n',
    'total': 9,
    'bottom': 9,
    'top': 1,
}

API происхождения и интеграция сторонних систем

Шаблоны Django содержат объект Origin, доступный через атрибут template.origin. Это позволяет отображать отладочную информацию в отчёте об ошибках шаблона, а также в сторонних библиотеках, таких как Django Debug Toolbar.

Пользовательские движки могут предоставлять собственную информацию template.origin путём создания объекта, определяющего следующие атрибуты:

  • 'name': Полный путь к шаблону.
  • 'template_name': Относительный путь к шаблону, переданный в методы загрузки шаблонов.
  • 'loader_name': Необязательная строка, идентифицирующая функцию или класс, используемые для загрузки шаблона, например django.template.loaders.filesystem.Loader.

Язык шаблонов Django

Синтаксис

О данной секции

Это обзор синтаксиса языка шаблонов Django. Подробности см. в справочнике по синтаксису языка.

Шаблон Django — это просто текстовый документ или строка Python, разметка которого выполняется с помощью языка шаблонов Django. Некоторые конструкции распознаются и интерпретируются движком шаблонов. Основными из них являются переменные и теги.

Шаблон отображается с контекстом. Отображение подставляет переменные с их значениями, которые ищутся в контексте, и выполняет теги. Всё остальное выводится как есть.

Синтаксис языка шаблонов Django включает четыре конструкции.

Переменные

Переменная выводит значение из контекста, который представляет собой объект, подобный словарю, сопоставляющий ключи со значениями.

Переменные заключены в {{ и }}:

My first name is {{ first_name }}. My last name is {{ last_name }}.

С контекстом {'first_name': 'John', 'last_name': 'Doe'}, этот шаблон отобразится так:

My first name is John. My last name is Doe.

Доступ к элементам словаря, атрибутам и индексам списков выполняется с помощью обозначения точек:

{{ my_dict.key }}
{{ my_object.attribute }}
{{ my_list.0 }}

Если переменная имеет вызываемый объект, система шаблонов вызовет его без аргументов и воспользуется результатом вместо вызываемого объекта.

Теги

Теги обеспечивают произвольную логику в процессе отображения.

Это определение преднамеренно расплывчато. Например, тег может выводить содержимое, служить структурой управления (например, оператором «if» или циклом «for»), извлекать содержимое из базы данных или даже обеспечивать доступ к другим тегам шаблонов.

Теги заключены в {% и %}:

{% csrf_token %}

Большинство тегов принимают аргументы:

{% cycle 'odd' 'even' %}

Некоторые теги требуют начального и конечного тегов:

{% if user.is_authenticated %}Hello, {{ user.username }}.{% endif %}

Также доступна справочная информация по встроенным тегам, а также инструкции по написанию пользовательских тегов.

Фильтры

Фильтры преобразуют значения переменных и аргументов тегов.

Они выглядят так:

{{ django|title }}

С контекстом {'django': 'the web framework for perfectionists with deadlines'}, этот шаблон отобразится так:

The Web Framework For Perfectionists With Deadlines

Некоторые фильтры принимают аргумент:

{{ my_date|date:"Y-m-d" }}

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

Комментарии

Комментарии выглядят так:

{# this won't be rendered #}

Тег {% comment %} обеспечивает многострочные комментарии.

Компоненты

О данной секции

Это обзор API языка шаблонов Django. Подробности см. в справочнике по API.

Движок

django.template.Engine инкапсулирует экземпляр системы шаблонов Django. Основная причина прямого создания Engine заключается в использовании языка шаблонов Django вне проекта Django.

django.template.backends.django.DjangoTemplates — это тонкий обертка, адаптирующая django.template.Engine к API бэкенда шаблонов Django.

Шаблон

django.template.Template представляет собой скомпилированный шаблон. Шаблоны получаются с помощью Engine.get_template() или Engine.from_string()

Также django.template.backends.django.Template — это тонкий обертка, адаптирующая django.template.Template к общему API шаблонов.

Контекст

django.template.Context хранит некоторые метаданные помимо данных контекста. Он передаётся методу Template.render() для отображения шаблона.

django.template.RequestContext — подкласс Context, который хранит текущий HttpRequest и запускает обработчики контекста шаблонов.

В общем API нет эквивалентного понятия. Данные контекста передаются в обычном dict, а текущий HttpRequest передаётся отдельно при необходимости.

Загрузчики

Загрузчики шаблонов отвечают за поиск шаблонов, их загрузку и возврат объектов Template.

Django предоставляет несколько встроенных загрузчиков шаблонов и поддерживает пользовательские загрузчики шаблонов.

Обработчики контекста

Обработчики контекста — это функции, которые получают текущий HttpRequest в качестве аргумента и возвращают dict данных, которые должны быть добавлены в контекст отображения.

Их основное применение — добавление общих данных, используемых всеми шаблонами, в контекст, не дублируя код в каждом представлении.

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

© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/2.0/topics/templates/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API