Этот раздел описывает систему шаблонов Django с технической стороны – как она работает и как расширить её. Если вы ищите просто описание синтаксиса шаблонов, смотрите Язык шаблонов Django.
Этот документ подразумевает, что вы уже знакомы с шаблонами, контекстом, переменными, тегами и рендерингом. Если вам не знакомы эти концепции, начните с вступления в систему шаблонов Django.
Система шаблонов в Python работает в два этапа:
Вы настраиваете Engine.
Сначала вы компилируете код шаблона в объект Template.
Вы рендерите шаблон с Context.
Django использует высокоуровневый API, который не привязан к конкретному бэкенду:
Для каждого бэкенда DjangoTemplates из настройки the TEMPLATES, Django создает экземпляр Engine. DjangoTemplates оборачивает Engine, чтобы адаптировать его под API конкретного бэкенда шаблонов.
Модуль django.template.loader предоставляет функции, такие как get_template(), для загрузки шаблонов. Они возвращают django.template.backends.django.Template, который оборачивает django.template.Template.
Template, полученный на предыдущем шаге, содержит метод render(), который оборачивает контекст и запрос в Context и делегирует рендеринг основному объекту Template.
При создании Engine все аргументы должны передаваться как именованные:
dirs – это список каталого, в которых бэкенд ищет файлы шаблонов. Используется для настройки filesystem.Loader.
По умолчанию равен пустому списку.
app_dirs влияет только на значение loaders по умолчанию. Смотрите ниже.
По умолчанию False.
allowed_include_roots – список доступных префиксов для тега {% ssi %}. Эта настройка используется для безопасности, чтобы авторы шаблонов не имели доступа ко всем файлам.
Например, если 'allowed_include_roots' равна ['/home/html', '/var/www'], тогда {% ssi /home/html/foo.txt %} будет работать, а {% ssi /etc/passwd %} – нет.
По умолчанию равен пустому списку.
Не рекомендуется, начиная с версии 1.8: Опция allowed_include_roots устарела.
context_processors – список путей Python для импорта функций, которые используются для наполнения контекста шаблонов, если он рендерится с объектом запроса. Эти функции принимают объект запроса и возвращают dict значений, которые будут добавлены в контекст.
По умолчанию равен пустому списку.
Подробности смотрите в описании RequestContext.
debug – булево значение, которое включает и выключает режим отладки. При True шаблонизатор сохраняет дополнительную отладочную информацию, которая может использоваться для отображения информации ошибки, которая возникла во время рендеринга.
По умолчанию False.
loaders – список загрузчиков шаблонов, указанных строками. Каждый класс Loader знает как загрузить шаблоны из определенного источника. Вместо строки можно указать кортеж. Первым элементом должен быть путь к классу Loader, вторым – параметры, которые будут переданы в Loader при инициализации.
По умолчанию содержит список:
'django.template.loaders.app_directories.Loader', только если app_dirs равен True.
Подробности смотрите в Типы загрузчиков.
string_if_invalid значение, которые шаблонизатор выведет вместо неправильной переменной(например, с опечаткой в назчании).
По умолчанию – пустая строка.
Смотрите подробности в Как обрабатываются неправильные переменные.
file_charset – кодировка, которая используется при чтении файла шаблона с диска.
По умолчанию 'utf-8'.
Если проект настроен на использование DjangoTemplates и только его, этот метод вернет соответствующий Engine. Иначе вызовет исключение ImproperlyConfigured.
Необходим для совместимости API, который рассчитывает на глобальный, неявно настроенный механизм шаблонов. Не следует использовать для других ситуаций.
Загружает шаблон с указанным названием, компилирует его и возвращает объект Template.
Похож на get_template(), но принимает список шаблонов и возвращает первый доступный шаблон из списка.
Рекомендуемый метод создать Template – использовать методы-фабрики Engine: get_template(), select_template() и from_string().
В проекте, где настройка TEMPLATES содержит только DjangoTemplates, можно создать экземпляр Template напрямую.
Класс находится в django.template.Template. Конструктор принимает один аргумент – “сырой” код шаблона:
from django.template import Template
template = Template("My name is {{ my_name }}.")
За кулисами
Система парсит код шаблона один раз – когда вы создаете объект Template. Результат для оптимизации сохраняется в памяти как древовидная структура.
Сам по себе парсинг работает достаточно быстро, обычно с помощью небольших регулярных выражений.
Скомпилировав объект Template, вы можете отрендерить с его помощью контекст. Вы можете использовать один и тот же объект шаблона, чтобы отрендерить несколько контекстов.
Класс контекста находит в django.template.Context. Конструктор принимает два необязательных аргумента:
Словарь с переменными и их значениями.
Название текущего приложения. Оно помогает определить текущее пространство имен для URL-ов. Если вы не используете пространство имен для URL-ов, можете игнорировать этот аргумент.
Не рекомендуется, начиная с версии 1.8: Аргумент current_app устарел. Если он вам необходим, используйте RequestContext вместо Context.
Подробности смотрте ниже в Использование объекта Context.
Вызовите метод render() объекта Template с контекстом, чтобы “выполнить” шаблон:
>>> from django.template import Context, Template
>>> template = Template("My name is {{ my_name }}.")
>>> context = Context({"my_name": "Adrian"})
>>> template.render(context)
"My name is Adrian."
>>> context = Context({"my_name": "Dolores"})
>>> template.render(context)
"My name is Dolores."
Название переменной может состоять из букв (A-Z), цифр (0-9), подчеркивания (но не начинаться с подчеркивания) или точки.
У точки особое значение. Точка в названии переменной означает поиск. Встретив точку в названии переменной, система шаблонов пытается найти значение в следующем подряке:
Поиск в словаре. Например: foo["bar"]
Поиск атрибута. Например: foo.bar
Поиск в списке. Напрмиер: foo[bar]
Обратите внимание, “bar” в выражении {{ foo.bar }} будет интепретировано как строка “bar”, а не переменная с названием “bar”.
Система шаблонов будет использовать первое найденное значение. Вот несколько примеров:
>>> from django.template import Context, Template
>>> t = Template("My name is {{ person.first_name }}.")
>>> d = {"person": {"first_name": "Joe", "last_name": "Johnson"}}
>>> t.render(Context(d))
"My name is Joe."
>>> class PersonClass: pass
>>> p = PersonClass()
>>> p.first_name = "Ron"
>>> p.last_name = "Nasty"
>>> t.render(Context({"person": p}))
"My name is Ron."
>>> t = Template("The first stooge in the list is {{ stooges.0 }}.")
>>> c = Context({"stooges": ["Larry", "Curly", "Moe"]})
>>> t.render(c)
"The first stooge in the list is Larry."
Если найдена функция, или любой другой вызываемый объект, шаблон попытается вызвать её. Например:
>>> class PersonClass2:
... def name(self):
... return "Samantha"
>>> t = Template("My name is {{ person.name }}.")
>>> t.render(Context({"person": PersonClass2}))
"My name is Samantha."
Вызываемые переменные работают немного сложнее. Вам следует помнить о следующем:
Если выполнение функции вызвало исключение, это приведет к ошибке при выполнении шаблона, если только исключение не содержит атрибут silent_variable_failure равный True. В таком случае переменная будет отрендерена со значением опции string_if_invalid шаблонизатора (пустая строка по умолчанию). Например:
>>> 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(Exception):
... silent_variable_failure = True
>>> class PersonClass4:
... def first_name(self):
... raise SilentAssertionError
>>> p = PersonClass4()
>>> t.render(Context({"person": p}))
"My name is ."
Обратите внимание, django.core.exceptions.ObjectDoesNotExist, который является родительским для всех ошибок DoesNotExist в ORM, содержит silent_variable_failure = True. Поэтому, если вы используете объекты модели в шаблонах, исключение DoesNotExist будет проигнорировано.
Функция из переменной может быть вызвана, только если не требует обязательных аргументов. В таком случае шаблон вставит вместо переменной значение string_if_invalid.
Очевидно, что вызов функций может выполнять различные побочные действия, которые могут привести к уязвимостям, было бы глупо позволять шаблону выполнять их.
Хороший пример – метод delete() модели. Нельзя позволять шаблонам выполнять следующее:
I will now delete this valuable data. {{ data.delete }}
Чтобы избежать этого, укажите атрибут alters_data в функции или методе. Шаблон не будет вызывать переменную, если значение содержит alters_data=True, и будет использовать значение настройки string_if_invalid. Встроенные методы delete() и save() модели содержат атрибут alters_data=True. Например:
def sensitive_function(self):
self.database_record.delete()
sensitive_function.alters_data = True
В некоторых случая может понадобится отключить вызов переменной и использовать значение как есть. Для этого укажите атрибут do_not_call_in_templates со значением True. Шаблон будет интерпретировать такую функцию как не вызываемое значение (позволяя обратиться к её атрибутам, например).
Если переменная не найдена в шаблоне, будет использоваться значение опции string_if_invalid шаблонизатора, равной по умолчанию '' (пустая строка).
Фильтры, которые указаны для переменной, будут применяться, только если string_if_invalid равна '' (пустая строка). Если string_if_invalid равна другому значению, фильтры будут проигнорированы.
Теги if, for и regroup работают немного по другому. Если указать неправильную переменную, будет использоваться значение None. Фильтры всегда применяются к переменной в этих тегах.
Если string_if_invalid содержит '%s', будет подставлено название переменной.
Только для отладки!
Хотя string_if_invalid и полезная для отладки, лучше не менять её по умолчанию и использовать только при необходимости локально.
Многие шаблоны, включая шаблоны админки, полагаются на то, что неправильные переменные будут проигнорированы системой шаблонов. Если заменить '' в string_if_invalid на другое значение, могут возникнуть проблемы с рендерингом шаблонов.
Как правило string_if_invalid необходимо использовать для отладки конкретной проблемы в шаблоне, и после отладки возвращать значение по умолчанию.
Каждый контекст содержит True, False и None. Как и следовало ожидать эти переменные соответствуют объектам Python.
Система шаблонов Django не позволяет экранировать символы, которые используются в синтаксисе разметки шаблонов. Например, следует использовать тег templatetag, если необходимо вывести в шаблоне {% и %} как строки.
Аналогичные проблемы возникают, если необходимо использовать эти значения как аргумент фильтра или тега. Например, при парсинге блочного тега, Django ищет первое появление %} после {%. Таким образом нельзя использовать "%}" как текст. Например, в следующих ситуациях будет вызвано исключение TemplateSyntaxError:
{% include "template.html" tvar="Some string literal with %} in it." %}
{% with tvar="Some string literal with %} in it." %}{% endwith %}
Аналогичная проблема возникнет при использовании }} в качестве аргумента фильтра:
{{ some.variable|default:"}}" }}
Чтобы обойти эти ограничения, используйте переменную для хранения запрещенных строк, или свой теги или фильтр, чтобы обойти ограничения.
Обычно при создании объекта Context сразу передается словарь со всеми переменными. Но вы можете менять содержимое объекта Context и после его инициализации, использую стандартный API словарей:
>>> from django.template import Context
>>> c = Context({"foo": "bar"})
>>> c['foo']
'bar'
>>> del c['foo']
>>> c['foo']
''
>>> c['newvariable'] = 'hello'
>>> c['newvariable']
'hello'
Возвращает значение для key, если key находится в контексте, иначе возвращает otherwise.
Объект Context работает как стек. Поэтому можно использовать методы push() и pop(). Если вызывать pop() слишком часто, будет вызвано исключение django.template.ContextPopException:
>>> c = Context()
>>> c['foo'] = 'first level'
>>> c.push()
{}
>>> c['foo'] = 'second level'
>>> c['foo']
'second level'
>>> c.pop()
{'foo': 'second level'}
>>> c['foo']
'first level'
>>> c['foo'] = 'overwritten'
>>> c['foo']
'overwritten'
>>> c.pop()
Traceback (most recent call last):
...
ContextPopException
push() можно использовать как менеджер контекста, чтобы быть уверенным, что будет pop() вызван в конце.
>>> c = Context()
>>> c['foo'] = 'first level'
>>> with c.push():
>>> c['foo'] = 'second level'
>>> c['foo']
'second level'
>>> c['foo']
'first level'
Все аргументы push() будут переданы в конструктор dict при создании нового слоя в контексте.
>>> c = Context()
>>> c['foo'] = 'first level'
>>> with c.push(foo='second level'):
>>> c['foo']
'second level'
>>> c['foo']
'first level'
Кроме push() и pop() объект Context также предоставляет метод update(). Работает как и push(), но принимает словарь в качестве аргумента и добавляет его в стек.
>>> c = Context()
>>> c['foo'] = 'first level'
>>> c.update({'foo': 'updated'})
{'foo': 'updated'}
>>> c['foo']
'updated'
>>> c.pop()
{'foo': 'updated'}
>>> c['foo']
'first level'
Использовать Context, как стек, удобно в собственных тегах..
Метод flatten() возвращает весь стек Context одним словарём, включая встроенные переменные.
>>> c = Context()
>>> c['foo'] = 'first level'
>>> c.update({'bar': 'second level'})
{'bar': 'second level'}
>>> c.flatten()
{'True': True, 'None': None, 'foo': 'first level', 'False': False, 'bar': 'second level'}
Метод flatten() также используется для сравнения объектов Context внутри системы шаблонов.
>>> c1 = Context()
>>> c1['foo'] = 'first level'
>>> c1['bar'] = 'second level'
>>> c2 = Context()
>>> c2.update({'bar': 'second level', 'foo': 'first level'})
{'foo': 'first level', 'bar': 'second level'}
>>> c1 == c2
True
Результат flatten() можно использовать в тестах для сравнения Context и dict:
class ContextTest(unittest.TestCase):
def test_against_dictionary(self):
c1 = Context()
c1['update'] = 'value'
self.assertEqual(c1.flatten(), {
'True': True,
'None': None,
'False': False,
'update': 'value',
})
Django предоставляет специальный класс Context, django.template.RequestContext, который немного отличается от обычного django.template.Context. Первое отличие – он принимает HttpRequest первым аргументом. Например:
c = RequestContext(request, {
'foo': 'bar',
})
Еще одно отличие – он автоматически добавляет различные переменные в соответствии с опцией context_processors шаблонизатора.
context_processors содержит кортеж функций, которые называются процессорами контекста. Они принимают объект запроса в качестве аргумента и возвращают словарь переменных, которые будут добавлены в контекст. По умолчанию context_processors равна:
[
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
]
Встроенные процессоры контекста были перенесены из django.core.context_processors в django.template.context_processors в Django 1.8.
Кроме этого RequestContext всегда использует django.core.context_processors.csrf. Этот процессор контекста используется для безопасности админкой и другими встроенными приложениями. Чтобы исключить его случайное отключение, он захардкоден и не может быть выключен с помощью настройки context_processors.
Процессоры контекста применяются по очереди. Это означает, что один процессор может перетереть переменную, которую добавил предыдущий. Процессоры по умолчанию описаны ниже.
Когда применяются процессоры контекста
Процессоры контекста применяются после инициализации контекста. То есть процессор может перезаписать переменную, которую вы добавили в Context или RequestContext. Поэтому избегайте названий переменных, которые используются процессорами.
Если вы хотите, чтобы переменная контекста перезаписывала переменные процессора контекста, используйте следующий подход:
from django.template import RequestContext
request_context = RequestContext(request)
request_context.push({"my_name": "Adrian"})
Django использует такой способ, чтобы перезаписать процессоры контекста во внутреннем API, например render() и TemplateResponse.
Также в RequestContext можно передать список дополнительных процессоров контекста, используя третий необязательный аргумент processors. В это примере в RequestContext будет добавлена переменная ip_address:
from django.http import HttpResponse
from django.template import RequestContext
def ip_address_processor(request):
return {'ip_address': request.META['REMOTE_ADDR']}
def some_view(request):
# ...
c = RequestContext(request, {
'foo': 'bar',
}, [ip_address_processor])
return HttpResponse(t.render(c))
Вот список процессоров контекста по умолчанию:
Если включить этот процессор, в RequestContext будут добавлены следующие переменные:
user – объект auth.User текущего авторизованного пользователя или объект AnonymousUser, если пользователь не авторизованный).
perms – объект django.contrib.auth.context_processors.PermWrapper, которые содержит права доступа текущего пользователя.
Если включить этот процессор, в RequestContext будут добавлены следующие переменные, но только при DEBUG равном True и, если IP адрес запроса (request.META['REMOTE_ADDR']) указан в INTERNAL_IPS:
debug – True. Вы можете использовать эту переменную, чтобы определить DEBUG режим в шаблоне.
sql_queries – список словарей {'sql': ..., 'time': ...}, который содержит все SQL запросы и время их выполнения, которые были выполнены при обработке запроса. Список отсортирован в порядке выполнения SQL запроса.
Если включить этот процессор, в RequestContext будут добавлены следующие переменные:
LANGUAGES – значение настройки LANGUAGES.
LANGUAGE_CODE – request.LANGUAGE_CODE, если существует. Иначе значение LANGUAGE_CODE.
Смотрите Интернационализация и локализация.
Если включить этот процессор, в RequestContext будет добавлена переменная MEDIA_URL, которая содержит значение MEDIA_URL.
Если включить этот процессор, в RequestContext будет добавлена переменная STATIC_URL, которая содержит значение STATIC_URL.
Этот процессор добавляет токен, который используется тегом csrf_token для защиты от CSRF атак.
Если включить этот процессор, в RequestContext будет добавлена переменная request, содержащая текущий HttpRequest.
Если включить этот процессор, в RequestContext будут добавлены следующие переменные:
messages – список сообщений (строки), которые были добавлены с помощью фреймворка сообщений.
DEFAULT_MESSAGE_LEVELS – словарь приоритетов сообщений и их числовых кодов.
Была добавлена переменная DEFAULT_MESSAGE_LEVELS.
Интерфейс процессора контекста очень простой: это функция Python, которая принимает один аргумент, объект HttpRequest, и возвращает словарь, которая будет добавлен в контекст шаблона. Процессор контекста обязательно должен возвращать словарь.
Код процессора может находится где угодно. Главное не забыть указать его в опции 'context_processors' настройки:setting:TEMPLATES, или передать аргументом context_processors в Engine.
Обычно при разработке проекта шаблоны хранятся в файлах, а не создаются с помощью API Template. Сохраняйте их в каталоге, который называют каталог с шаблонами.
Django ищет каталоги с шаблонами в соответствии с настройками загрузки шаблонов (смотрите “Типа загрузчиков” ниже). Самый простой способ – указать каталоги с шаблонами в опции DIRS.
По умолчанию использует значение настройки TEMPLATE_DIRS.
Вы можете указать Django каталоги с шаблонами через опцию DIRS настройки TEMPLATES, или в аргументе dirs при создании Engine. Настройка должна содержать список или кортеж полных путей к каталогам. Например:
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
'/home/html/templates/lawrence.com',
'/home/html/templates/default',
],
},
]
Шаблоны могут находиться где угодно, главное, чтобы у Web-сервера были права на чтение. Расширение файла может быть любым, .html или .txt, или вообще без расширения.
Обратите внимание, пути должны быть Unix-стиле, даже для Windows (то есть использовать /).
По умолчанию Django использует загрузчик шаблонов с файловой системы, но Django предоставляет и другие загрузчики шаблонов, которые позволяют загружать шаблоны с других источников.
Некоторые из них выключены по умолчанию, но вы можете активировать их изменив опцию 'loaders' бэкенда DjangoTemplates в настройке TEMPLATES, или передав аргумент loaders в Engine. Опция loaders содержит кортеж строк, каждая из которых представляет класс загрузчика шаблонов. Вот список загрузчиков, которые предоставляет Django:
django.template.loaders.filesystem.Loader
Загружает шаблоны с файловой системы в соответствии с настройкой DIRS.
Этот загрузчик включен по умолчанию. Однако, он не найдет ни один шаблон, пока вы не укажите список каталогов в DIRS:
TEMPLATES = [{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')],
}]
django.template.loaders.app_directories.Loader
Загружает шаблоны из каталога приложения Django. Для каждого приложения в INSTALLED_APPS загрузчик ищет под-каталог templates. Если под-каталог найден, Django ищет в нем шаблон.
Это означает, что вы можете хранить шаблоны вместе с приложением. Таким образом легко распространять приложение Django с шаблонами по умолчанию.
Например для следующих настроек:
INSTALLED_APPS = ('myproject.polls', 'myproject.music')
... get_template('foo.html') будет искать foo.html в таких каталогах в указанном порядке:
... и будет использовать первый найденный.
Порядок INSTALLED_APPS – важен! Например, вы хотите переопределить шаблон админки Django, например admin/base_site.html из django.contrib.admin, заменив на admin/base_site.html из myproject.polls. Вы должны указать myproject.polls перед django.contrib.admin в INSTALLED_APPS, иначе шаблон из django.contrib.admin будет загружен первым, а ваш проигнорирован.
Обратите внимание, загрузчик выполняет некоторую оптимизацию при первом импорте: он кеширует список приложений из INSTALLED_APPS, которые содержат под-каталог templates.
Вы можете включить этот загрузчик, указав True в APP_DIRS:
TEMPLATES = [{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'APP_DIRS': True,
}]
django.template.loaders.eggs.Loader
Аналогичен app_directories, но загружает шаблоны из Python eggs, а не файловой системы.
Загрузчик выключен по умолчанию.
django.template.loaders.cached.Loader
По умолчанию система шаблонов читает и компилирует ваш шаблон при каждом рендеринге шаблона. Хотя система шаблонов Django работает достаточно быстро, но общие накладные расходы на чтение и компилирование шаблонов могут быть существенны.
Кеширующий загрузчик шаблонов принимает список загрузчиков Он будет использовать их для поиска неизвестных шаблонов, которые загружаются первый раз. Затем скомпилированные Template сохраняются в памяти. Закешированный объект Template возвращается при повторном поиске уже загруженного шаблона.
Например, чтобы включить кеширование с загрузчиками filesystem и app_directories, используйте следующие настройки:
TEMPLATES = [{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')],
'OPTIONS': {
'loaders': [
('django.template.loaders.cached.Loader', [
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
]),
],
},
}]
Примечание
Все встроенные теги Django можно использовать с кеширующим загрузчиком, но теги сторонних приложений, или ваши собственные, должны использовать потокобезопасный код при использовании класса Node. Смотрите Потокобезопасные шаблонные теги.
Загрузчик выключен по умолчанию.
django.template.loaders.locmem.Loader
Загружает шаблоны из словаря Python. Удобен при тестировании.
Этот загрузчик принимает словарь каталогов первым аргументом:
TEMPLATES = [{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'OPTIONS': {
'loaders': [
('django.template.loaders.locmem.Loader', {
'index.html': 'content here',
}),
],
},
}]
Загрузчик выключен по умолчанию.
Django использует загрузчики шаблонов в порядке, указанном в опции 'loaders'. Загрузчики используются пока один из них не найдет шаблон.
Наш класс Loader должен наследоваться от django.template.loaders.base.Loader и переопределять метод load_template_source(), который принимает аргумент template_name, загружает шаблон с файловой системы (или другого источника), и возвращает кортеж: (template_string, template_origin).
django.template.loaders.base.Loader ранее находился в django.template.loader.BaseLoader.
Метод load_template() класса Loader загружает содержимое шаблона, используя load_template_source(), создает экземпляр Template с этим содержимым, и возвращает кортеж: (template, template_origin).
Если Engine инициализирован с debug=True, его шаблоны содержат атрибут origin, который указывает откуда был загружен шаблон. Для шаблонизаторов, инициализированных Django, debug по умолчанию равен DEBUG.
Шаблоны, загруженные с помощью загрузчика шаблонов, будут использовать класс django.template.loader.LoaderOrigin как значение этого атрибута.
Путь к шаблону, который вернул загрузчик шаблонов. Для загрузчиков, которые ищут в файловой системе, это будет полный путь к файлу шаблона.
Относительный путь к шаблону, которые передается в загрузчик шаблонов.
Jun 02, 2016