#29. Templates

Templates (они же Темплейты, они же Шаблоны)

enter image description here

План

I. Шаблон

  1. Что такое шаблон и как его подключать в Django
  2. Альтеранативный вариант расположения templates
  3. Функция render

II. Django Template Language or DTL

  1. Переменные
  2. Теги
  3. Логические операторы
  4. Циклы
  5. url
  6. Комментарии
  7. Фильтры
  8. Наследование шаблонов
  9. Практика по наследованию
  10. extend и include
  11. Практика по наследованию

III. Литература (что почитать)

Что такое шаблон

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

В папке приложения необходимо создать папку templates, и в ней нужно создать html файл, назовём его index.html (название не имеет значения, главное, чтобы формат был html)

mysite/
    myapp/
        templates/
            index.html
manage.py

С таким содержанием:

<!DOCTYPE html>  
<html lang="en">  
<head>  
  <meta charset="UTF-8">  
  <title>Title</title>  
</head>  
<body>  
  
Hello, world!  
<br>  
That's a template!  
  
</body>  
</html>

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

В файле myapp/views.py необходимо импортировать обработчик шаблонов, для этого в начале файла добавляем

from django.shortcuts import render

И перепишем функцию index:

def index(request):
    return render(request, 'index.html')

Обновим страницу в браузере и увидим результат
enter image description here

Альтеранативный вариант расположениея templates

Создадим новую папку на уровне корня проекта и назовём её templates (название может быть любым, но принято называть именно так), чтобы получилась вот такая структура:

mysite/
    myapp/
    templates/
    manage.py

Для того, чтобы обрабатывать шаблоны мы должны “рассказать” Django, где именно искать эти самые шаблоны. Для этого нужно открыть файл mysite/settings.py и отредактировать его.

В данный момент нас интересует переменная TEMPLATES, выглядит это примерно так:
В ключ DIRS добавим нашу папку с шаблонами, чтобы получилось так:

TEMPLATES = [  
    {  
        'BACKEND': 'django.template.backends.django.DjangoTemplates',  
        'DIRS': [BASE_DIR / 'templates']  
        ,  
        'APP_DIRS': True,  
        'OPTIONS': {  
            'context_processors': [  
                'django.template.context_processors.debug',  
                'django.template.context_processors.request',  
                'django.contrib.auth.context_processors.auth',  
                'django.contrib.messages.context_processors.messages',  
            ],  
        },  
    },  
]

Ключи:

BACKEND - путь к классу, который отвечает за обработку данных и логику (замена требуется очень редко).

DIRS - список папок, в которых Django будет искать шаблоны.

APP_DIRS - булевое поле которое отвечает за то, нужно ли искать папки с шаблонами внутри папки с приложениями, например в нашей структуре, если значение False, то поиск будет только в папке templates на уровне файла manage.py, а если значение True то в папках /templates и /myapp/templates/

OPTIONS - дополнительные настройки.

render()

from django.shortcuts import render

render(request, template_name, context=None, content_type=None, status=None, using=None)

Объединяет заданный шаблон с заданным контекстным словарем и возвращает объект HttpResponse с этим визуализированным кодом.

Обязательные аргументы

request

Объект запроса, использованный для генерации этого ответа.

template_name

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

Необязательные аргументы

context

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

content_type

Тип MIME для использования в итоговом документе. По умолчанию - text/html.

status

Код состояния для ответа. По умолчанию 200.
Если передать, например, 500, в консоли браузера можно увилеть подобные ошибки
enter image description here

using

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

Для демонстрации основных типов данных допишем функцию index и передам большое кол-во значений в словаре:

class MyClass:
    string = ''

    def __init__(self, s):
        self.string = s

def index(request):
    my_num = 33
    my_str = 'some string'
    my_dict = {"some_key": "some_value"}
    my_list = ['list_first_item', 'list_second_item', 'list_third_item']
    my_set = {'set_first_item', 'set_second_item', 'set_third_item'}
    my_tuple = ('tuple_first_item', 'tuple_second_item', 'tuple_third_item')
    my_class = MyClass('class string')
    data = {
        'my_num': my_num,
        'my_str': my_str,
        'my_dict': my_dict,
        'my_list': my_list,
        'my_set': my_set,
        'my_tuple': my_tuple,
        'my_class': my_class,
    }
    return render(
        request,
        'index.html',
        data
    )    

Значения переданы, но пока они никак не используются, давай те же посмотрим, как отобразить переменные в шаблоне!

Django Template Language or DTL

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

Переменные

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

Переменные заключаются в специальные символы - двойные фигурные скобки {{ и }}, например:

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 }}

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

Изменим index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div>
    <div style="border: 1px darkblue solid">
        {{ my_num }}
    </div>
    <div style="border: 1px darkseagreen solid">
        {{ my_str }}
    </div>
    <div style="border: 1px fuchsia solid">
        {{ my_set }}
    </div>
    <div style="border: 1px firebrick solid">
        {{ my_dict.some_key }}
    </div>
    <div style="border: 1px cyan solid">
        {{ my_class.string }}
    </div>
    <div style="border: 1px cyan solid">
        {{ my_list.0 }}
    </div>
    <div style="border: 1px burlywood solid">
        {{ my_tuple.1 }}
    </div>
</div>
</body>
</html>

enter image description here

Теги

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

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

Теги заключаются в символы {% и %}, например:

{% csrf_token %}

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

{% cycle 'odd' 'even' %}

Для некоторых тегов требуются начальные и конечные теги:

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

Логические операторы

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

Давайте добавим в наши параметры переменную display_num и назначим ей False

myapp/views.py

data = {  
    'my_num': my_num,  
    'my_str': my_str,  
    'my_dict': my_dict,  
    'my_list': my_list,  
    'my_set': my_set,  
    'my_tuple': my_tuple,  
    'my_class': my_class,  
    'display_num': False,  
}

Для логических условий и циклов используются другие скобки {% %}

Изменим наш шаблон с использованием логики:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div>
    {% if display_num %}
        {{ my_num }}
    {% else %}
        <span> We don't display num </span>
    {% endif %}
</div>
</body>
</html>

Обновим страницу и увидим:
enter image description here

А если изменим только во views переменную с False на True:

myapp/views.py

То увидим:
enter image description here

Т.к. переменная True, то мы видим значение переменной my_num

Циклы

Так же как и в python мы можем использовать циклы в шаблонах, но только цикл for

Изменим index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div>
    {% for item in my_list %}
        <span>{{ item }}</span>
        <br>
    {% endfor %}
</div>
</body>
</html>

Обновим страницу и увидим:
enter image description here
Еще раз, логика через {% %}, данные через {{ }}

Давайте скомбинируем!

Внутри цикла for Django уже генерирует некоторые переменные, например переменную
{{ forloop.counter0 }}, в которой хранится индекс текущей итерации, давайте не будем выводить в цикле второй элемент (индексация начинается с 0)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div>
    {% for item in my_list %}
        {% if forloop.counter0 != 1 %}
            <span>{{ item }}</span>
            <br>
        {% endif %}
    {% endfor %}
</div>
</body>
</html>

Обновляем страницу и видим:
enter image description here

url

Тег url позволяет нам сгенерировать урл по его имени. Это очень удобно, если адрес меняется, а его имя нет.

Давайте сгенерируем две ссылки на два наших урла.

mysite/urls.py

Назначим имя index

path('', index, name='index')

myapp/urls.py

Назначим имя first

path('', first, name='first')

Добавим в наш шаблон ссылку на вторую страницу:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div>
    <a href="{% url 'first' %}">Another page</a>
</div>
</body>
</html>

enter image description here

Комментарии

Однострочный

{# this won't be rendered #}

Многострочный при помощи тега comment

{% comment "Optional note" %}
    <p>Commented out text</p>
{% endcomment %}

Фильтры

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

Выглядят они так:

{{ django|title }}

В контексте {'django': 'the web framework for perfectionists with deadlines'}, этот шаблон отображает:

The Web Framework For Perfectionists With Deadlines

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

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

enter image description here
Справочник по встроенным фильтрам,
а также инструкции по написанию пользовательских тегов и фильтров.

Наследование шаблонов

enter image description here
Наши предыдущие примеры шаблонов были небольшими фрагментами HTML-кода, однако в реальной ситуации вы будете использовать Django для создания больших страниц. Отсюда возникает один из наиболее существенных вопросов веб-разработки — как уменьшить количество повторяющегося и избыточного кода в общих частях страниц, таких как header, footer, навигация по сайту?

Основная фишка шаблонов Django — наследование. Шаблон может расширять (уточнять) поведение родительского шаблона.

Любой участок шаблона может быть обернут в блочный тег (естественно, что тег не может начинаться перед, а заканчиваться внутри цикла). Блоку дается имя.
Например:

{% block content %}
	тело блока
{% endblock %}

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

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

Пример наследования шаблонов

Допустим, мы хотим сделать сайт, содержащий простые страницы и блог.

От верстальщика мы получили макет страницы, содержащий:

  • header - шапку (логотип, заголовок страницы, меню);
  • body - тело страницы;
  • footer - «подвал» с информацией о правах распространения.

Вот как это выглядит:

|

{% block head %}
	{% block title %}{% endblock %}
	{% block menu %}{% endblock %}
{% endblock %}

{% block page %}
	{% block content %}
	{% endblock %}
{% endblock %}

{% block footer %}
	{% block copyright %}
	{% endblock %}
{% endblock %}

Для всех указанных элементов мы создаем соответствующие блочные теги.

Простая страница ложится в этот макет — у нее есть только заголовок и тело.

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

{% extend "base.htm" %}

{% block page %}
	{% block content %}
	{% endblock %}

	{% block sidebar %}
	{% endblock %}
{% endblock %}

В блоге будет несколько типов страниц:

  • список статей;
  • статья;
  • список тегов;
  • список статей, у которых есть определенный тег;

У всех страниц правая колонка остается неизменной, поэтому разумно сделать базовую страницу для блога, наследуя ее от двухколоночной базовой страницы.

{% extend "base_2col.htm" %}

{% block title %}
	Блог
{% endblock %}

{% block sidebar %}
	{% block tags %}
	{% endblock %}

	{% block recent %}
	{% endblock %}
{% endblock %}

Теперь приведем примеры внутренних страниц блога (все они наследуются от базовой страницы блога).

Список статей:

{% extend "blog/base.htm" %}

{% block content %}
	{% for article in article_list %}
		<a href="{% url article_view article.id %}">
		{{ article.title }}
		</a>
	{% endfor %}
{% endblock %}

Статья:

{% extend "blog/base.htm" %}

{% block title %}
	{{ article.title }} - {{ block.super }}
{% endblock %}

{% block content %}
	{{ article.text }}
{% endblock %}

Список статей, у которых есть определенный тег:

{% extend "blog/index.htm" %}

{% block title %}
	{{ tag.title }} - {{ block.super }}
{% endblock %}

{% block content %}
	{{ tag.title }}
	{{ tag.text }}

	{{ block.super }}
{% endblock %}

В данном случае, мы воспользовались еще одной хитростью. Ведь этот список ничем не отличается от простого списка статей — он просто отфильтрован по дополнительным параметрам. Поэтому мы унаследовали его от списка статей и при перекрытии тела использовали тег {{ block.super }} — вывести все содержимое родительского блока.

Как видно, каждый шаблон очень конкретен и отвечает только за свою функциональность. Ему нет необходимости знать о всей странице в целом.

Практика по наследованию

Создадим в папке templates новые файлы base.html и first.html и изменим файлы templates/index.html и функцию first в myapp/views.py

template/base.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div style="background-color: aqua">
    {% block content %}
    {% endblock %}
</div>
</body>
</html>

template/index.html

{% extends 'base.html' %}

{% block content %}
<div style="padding: 20px; background-color: fuchsia"> Extended template index!</div>
<a href="{% url 'first' %}">To the first page</a>
{% endblock %}

template/first.html

{% extends 'base.html' %}

{% block content %}
<div style="padding: 20px; background-color: chocolate"> Extended template first!</div>
<a href="{% url 'index' %}">To the index page</a>
{% endblock %}

Функция first в myapp/views.py

def first(request):
    return render(request, 'first.html')

Смотрим результаты произошедшего и пытаемся их понять (ссылки добавлены для удобства).

Индекс страница
enter image description here

Первая страница
enter image description here

Что произошло?

Мы создали базовый шаблон base.html в котором описали то, что будет во всех шаблонах (и покрасили в голубенький, для наглядности), которые от него наследуются, и обозначили {% block content %}. Вся нужная нам информация будет наследоваться именно в указанный блок, на странице разных блоков может быть сколько угодно главное, чтобы они имели разные названия.

Наш index view рендерит страницу index.html в ней мы отнаследовались от нашей base.html вписали такой же блок content чтобы в него вписать нужные нам данные, в нашем случае это просто текст и ссылка, текст мы перекрасили, чтобы было видно что это данные из нового файла, а ссылку - нет, чтобы было видно, что цвет из base.html отнаследовался, то же самое произошло и с first.html

Include

А теперь представим обратную ситуацию, нам нужно в разные части сайта вставить один и тот же блок (рекламу, например)

Тут нас спасает тег include который позволяет “внедрить” нужную часть страницы куда угодно

Создадим в папке templates еще один файл с названием add.html

templates/add.html

<div style="padding: 20px; background-color: chartreuse"> That's included html!!!</div>

И теперь добавим этот файл к страницам index.html и first.html но в разные места, чтобы получилось

template/index.html

{% extends 'base.html' %}

{% block content %}
{% include 'add.html' %}
<div style="padding: 20px; background-color: fuchsia"> Extended template index!</div>
<a href="{% url 'first' %}">To the first page</a>
{% endblock %}

template/first.html

{% extends 'base.html' %}

{% block content %}
<div style="padding: 20px; background-color: chocolate"> Extended template first!</div>
<a href="{% url 'index' %}">To the index page</a>
{% include 'add.html' %}
{% endblock %}

Смотрим на результат:

index:
enter image description here

first:

В добавленную станицу можно передать переменные, при помощи тега with

Изменим файл templates/add.html

<div style="padding: 20px; background-color: chartreuse"> Hello {{ name }} !</div>

И файл templates/index.html

{% extends 'base.html' %}

{% block content %}
{% include 'add.html' with name='world' %}
<div style="padding: 20px; background-color: fuchsia"> Extended template index!</div>
<a href="{% url 'first' %}">To the first page</a>
{% endblock %}

Смотрим на результат:

index
enter image description here

first
enter image description here

В первом случае мы видим, что добавлена переменная, во втором - ничего, так как мы ничего не передавали.

Давайте добавим проверку на наличие переменной!

templates/add.html

<div style="padding: 20px; background-color: chartreuse">{% if name %} Hello {{ name }} ! {% else %} Sorry I don't know your name {% endif %}
</div>

Смотрим на first page
enter image description here

Переменной нет, срабатывает if

Так же можно передавать эту переменную из view, для этого нужно в with дописать {{ variable_name }}

Домашнее задание / Практика

Создать базовую html от которой будут наследоваться все остальные

Для статических урлов сделать html файлы наследующиеся от базового, но с разным текстом (можно и оформлением)

Для урлов http://127.0.0.1:8000/article/<int:article_number>, http://127.0.0.1:8000/article/<int:article_number>/<slug:slug_text>,

cделать html файлы в которых выводить текст о том, чётный введен или нечётный article_number (логику прописать в темплейтах), если введён slug_text, выводить этот текст при помощи include в добавочной html (добавленной из отдельного файла).

На главной странице (http://127.0.0.1:8000/) сделать две ссылки, перейти на случайную статью (id), и перейти на случайную статью со случайным слагом (5-10 случайных символов)

На всех страницах внизу должна быть ссылка на главную.

Литература

Документация

  1. 📖 Язык шаблонов Django
  2. 📖 Встроенные теги и фильтры шаблонов