#29. Django Templates

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

План

I. Шаблон

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

II. Django Template Language або DTL

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

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

Що таке template

Template (Шаблон) - це текстовий файл, який визначає структуру і розташування даних у файлі, крім того, в ньому розміщують спеціальні теги, які використовуються для показу реального вмісту, тобто даних. За замовчуванням 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')

Оновимо сторінку в браузері і побачимо результат

Альтеранативний варіант розташування 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, у консолі браузера можна побачити подібні помилки
введіть тут опис зображення

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 або 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>

Теги

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

Це визначення свідомо розпливчасте. Наприклад, тег може виводити контент, слугувати структурою управління, наприклад, оператор “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>

Оновимо сторінку і побачимо:

А якщо змінимо тільки у views змінну з False на True:

myapp/views.py

То побачимо:

Оскільки змінна 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>

Оновимо сторінку і побачимо:

Ще раз, логіка через {% %}, дані через {{ }}

Давайте скомбінуємо!

Усередині циклу 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>

Оновлюємо сторінку і бачимо:

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>

Коментарі

Однорядковий

{# 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" }}


Довідник із вбудованих фільтрів,
а також інструкції з написання користувацьких тегів і фільтрів.

Успадкування шаблонів

введіть тут опис зображення
Наші попередні приклади шаблонів були невеликими фрагментами HTML-коду, однак у реальній ситуації ви використовуватимете Django для створення великих сторінок. Звідси виникає одне з найсуттєвіших питань веб-розробки - як зменшити кількість повторюваного та надлишкового коду в загальних частинах сторінок, таких як header, footer, навігація по сайту?

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

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

{% block content %}
	тіло блоку
{% endblock %}

Extends

За допомогою тега extends ми вказуємо, який шаблон ми будемо уточнювати. Розширюючи шаблон, ми можемо перевизначити будь-які блоки, які є в батьківському шаблоні. Усе, що знаходиться поза цими блоками, буде пропущено.

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

Приклад успадкування шаблонів

Припустімо, ми хочемо зробити сайт, що містить прості сторінки і блог.

Від верстальника ми отримали макет сторінки, що містить:

  • header - шапку (логотип, заголовок сторінки, меню);
  • body - тіло сторінки;
  • footer - “підвал” з інформацією про права поширення.

Ось як це виглядає:

|

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

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

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

Для всіх зазначених елементів ми створюємо відповідні блокові теги.

Проста сторінка лягає в цей макет - у неї є тільки заголовок і тіло.

Тепер перейдемо до блогу. У блозі хотілося б додати праву колонку для виведення списку тегів і останніх статей. Можливо ми захочемо додати праву колонку до якихось інших сторінок сайту. Щоб уникнути копіювання “двоколоночності”, винесемо її в окремий шаблон, перевизначивши тіло сторінки у базового.

{% extends "base.htm" %}

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

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

У блозі буде кілька типів сторінок:

  • список статей;
  • стаття;
  • список тегів;
  • список статей, у яких є певний тег;

У всіх сторінок права колонка залишається незмінною, тому розумно зробити базову сторінку для блогу, успадкувавши її від двоколоночної базової сторінки.

{% extends "base_2col.htm" %}

{% block title %}
	Blog
{% endblock %}

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

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

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

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

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

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

Стаття:

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

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

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

Список статей, у яких є певний тег:

{% extends "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')

Дивимося результати події і намагаємося їх зрозуміти (посилання додано для зручності).

index:

first:

Що сталося?

Ми створили базовий шаблон 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:

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:

first:

У першому випадку ми бачимо, що додано змінну, у другому - нічого, оскільки ми нічого не передавали.

Давайте додамо перевірку на наявність змінної!

templates/add.html

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

Дивимося на first page

Змінної немає, спрацьовує 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>

зробити html-файли, в яких виводити текст про те, парний введений або непарний article_number (логіку прописати в темплейтах), якщо введено slug_text, виводити цей текст за допомогою include у додатковій html (доданій з окремого файлу).

На головній сторінці (http://127.0.0.1:8000/) зробити два посилання:

  1. перейти на випадкову статтю (id)
  2. перейти на випадкову статтю з випадковим слагом (5-10 випадкових символів)

На всіх сторінках внизу має бути посилання на головну.

Література

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

  1. 📖 Мова шаблонів Django
  2. 📖 Вбудовані теги та фільтри шаблонів