Код
#статьи

Пишем приложение на Django. Часть 2. Работаем над фронтендом и админкой

Продолжаем писать приложение на фреймворке Django — тюним внешний вид блога, добавляем кнопки и настраиваем админку.

Иллюстрация: Colowgee для Skillbox Media

В первой части урока по Django мы установили фреймворк, написали движок для блогов, определили компоненты записей и научились постить в него записи. Теперь же поработаем над фронтендом: сделаем посты более аккуратными и симпатичными, добавим кнопки для работы с контентом и улучшим UX. Начнём сразу с четвёртого шага — первые три как раз были в предыдущей статье :)

Шаг 4


Работаем над фронтендом: настраиваем отображение записей блога

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

Создаём список и настраиваем представления (Views)

В Django есть два подхода к созданию представлений: на основе функций и на основе классов. Они работают одинаково: принимают запрос и возвращают ответ. Разница между ними в том, что представления на основе функций позволяют точнее контролировать работу, но придётся писать больше кода.

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

Используя Django, можно применять встроенные типовые представления. Мы выберем подклассы DetailView и ListView, подключив их к модели Entry в файле entries/views.py:

from django.views.generic import (
   ListView,
   DetailView,
)
 
from .models import Entry
 
class EntryListView(ListView):
   model = Entry
   queryset = Entry.objects.all().order_by("-date_created") #Это ключевой запрос — он
возвращает все существующие записи по нашему первичному ключу, сортируя их по дате создания.
 
class EntryDetailView(DetailView): #Дополнительно создаём представление с подклассом
DetailView. Будем использовать его позже.
   model = Entry

Создаём шаблон (Templates) в Django

В Django HTML-код генерируется автоматически с помощью шаблонов. Это очень удобно, когда нам надо создавать большое количество типового контента — например, записи в блоге.

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

% mkdir -p entries/templates/entries

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

Начнём с создания файла entry_list.html и добавим в него следующее содержимое:

{% for entry in entry_list %} #Создаём цикл, который будет анализировать 
каждую запись в нашем списке с записями.
<article>
   <h2 class="{{ entry.date_created|date:'l' }}">
       {{ entry.date_created|date:'Y-m-d H:i' }}
   </h2>
   <h3>
       <a href="{% url 'entry-detail' entry.id %}">
           {{ entry.title }}
       </a>
   </h3>
</article>
{% endfor %}

В шаблонах Django можно динамически ссылаться на классы CSS. Если вы посмотрите на <h2>, вы увидите, что к нему добавлен class="{{ entry.date_created|date: 'l' }}». Этот класс используется для отображения временной метки со специальным форматированием. Следовательно, нашему элементу <h2> в качестве класса теперь присвоен день недели — а значит, этот класс можно использовать, чтобы задавать уникальный стиль записям по дням недели. Этим мы займёмся немного позже, когда начнём оформлять блог.

Внутри цикла entry_list мы можем получить доступ ко всем полям модели Entry. Чтобы не загромождать список слишком большим количеством информации, мы будем показывать содержимое только при переходе на страницу записи. Поэтому создадим файл entry_detail.html в папке entries/templates/entries/ и добавим туда следующий код:

<article>
   <h2>{{ entry.date_created|date:'Y-m-d H:i' }}</h2> #Отображаем дату создания записи в формате год-месяц-день, час-минута.
   <h3>{{ entry.title }}</h3> #Отображаем заголовок записи.
   <p>{{ entry.content }}</p> #Отображаем содержимое записи.
</article>

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

Определяем маршруты (Roots) для наших записей

Чтобы шаблоны работали, нам необходимо связать созданные представления с URL-адресами. Django использует для этого файл urls.py, позволяющий обрабатывать входящие запросы от пользователя в браузере.

Файл urls.py был создан автоматически в папке blog. Однако для нашего приложения Entries нам необходимо снова создать его — теперь уже в папке entries — и добавить маршруты к представлениям EntryListView и EntryDetailView:

from django.urls import path
from . import views
 
urlpatterns = [
   path(
       "",
       views.EntryListView.as_view(),
       name="entry-list"
   ),
   path(
       "entry/<int:pk>",
       views.EntryDetailView.as_view(),
       name="entry-detail"
   ),
]

Функция path() должна иметь как минимум два аргумента для обоих представлений:

  • строковое имя маршрута, который содержит шаблон URL;
  • ссылку на представление в виде функции as_view(). Она используется для представлений на основе классов.

Дополнительно можно передать такие аргументы, как kwargs, и явно указать имя. С помощью имени легко ссылаться на представления в проекте Django. Таким образом, даже если потребуется изменить шаблон URL, не придётся обновлять сами шаблоны.

Теперь, когда URL-адреса для приложения записей созданы, нам нужно связать их со списком urlpatterns блога. Откройте файл diary/urls.py и найдите urlpatterns, которые использует наш проект. Сейчас в нём есть только один маршрут к admin/, который добавлен по умолчанию и позволяет нам попасть в панель администратора Django. Чтобы показать записи блога при посещении сайта http://localhost:8000, надо отправить корневой URL в созданное приложение Entries, изменив diary/urls.py:

​​from django.contrib import admin
from django.urls import path, include #Импортируем функцию include.
 
urlpatterns = [
   path("admin/", admin.site.urls),
   path("", include("entries.urls")), #Явно указываем на то, что требуется обратиться к приложению Entries.
]

Теперь запустите сервер и перейдите в панель администратора Django. Создайте любую запись в блоге:

Скриншот: Django / Skillbox Media

Сохраним её и перейдём на главную страницу блога — http://127.0.0.1:8000.

Скриншот: Django / Skillbox Media

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

Скриншот: Django / Skillbox Media

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

Шаг 5


Работаем над внешним видом блога

Блог должен быть привлекательным и удобным для читателей и автора. Чтобы улучшить его внешний вид, создадим базовый шаблон с единым оформлением по адресу entries/templates/entries/base.html:

{% load static %} #Об этой части кода поговорим в следующем пункте, пока просто напишем.
<!DOCTYPE html>
<head>
   <meta charset="UTF-8">
   <title>Мой блог</title>
   <link rel="stylesheet" href="{% static 'css/diary.css' %}"> #Ссылаемся на CSS-файл, который создадим позже.
</head>
<body>
   <h1><a href="/">Привет, читатель!</a></h1> #Добавляем общий заголовок на главную страницу.
 
   {% block content %}{% endblock %}
</body>
</html>

Особенность Django заключается в том, что при наследовании шаблонов нам не нужно повторять их разметку. Вместо этого мы просто дополняем дочерние шаблоны, а сам фреймворк автоматически объединяет их перед отображением.

Добавляем таблицу стилей

Вставив {% load static %} в начало файла шаблона, мы можем ссылаться на статические файлы с помощью тега шаблона {% static %} и относительного пути к файлу CSS.

Создадим файл diary.css в папке entries/static/css/ и добавим в него такой код:

* {
   box-sizing: border-box;
}
 
body {
   font-family: sans-serif;
   font-size: 18px;
}
 
a {
   color: inherit;
}
a:hover {
   opacity: 0.7;
}
 
h1 {
   font-size: 2.8em;
}
 
h1 a {
   text-decoration: none;
}
 
h2, h3 {
   font-size: 1.4em;
   margin: 0;
   display: inline-block;
   padding: 0.5rem 1rem;
   vertical-align: top;
}
 
h2 {
   background-color: aquamarine;
}
 
.mark {
   background-color: gainsboro;
}
.mark a {
   text-decoration: none;
}
 
article {
   margin-bottom: 0.5rem;
}
 
p {
   font-size: 1.2em;
   padding-left: 1rem;
   line-height: 1.3em;
   max-width: 36rem;
   color: dimgray;
}
 
em {
   font-style: normal;
   font-weight: bold;
}
 
/* Form */
 
label {
   display: block;
}
 
button,
textarea,
input {
   font-size: inherit;
   min-height: 2.5em;
   padding: 0 1rem;
}
 
input[type="text"],
textarea {
   width: 100%;
}
 
textarea {
   padding: 0.5rem 1rem;
   font-family: sans-serif;
}
 
button,
input[type="submit"] {
   margin: 0 1rem 2px 1rem;
   cursor: pointer;
   font-weight: bold;
   min-width: 8rem;
}
 
/* Добавим отдельный стиль оформления для записей, которые сделаны в выходные дни. */
.Saturday,
.Sunday {
   background-color: lightsalmon;
}

Редактируем дочерние шаблоны

Теперь свяжем дочерние шаблоны с родительским шаблоном base.html. Для этого обновим entries/templates/entries/entry_list.html, чтобы он выглядел следующим образом:

{% extends "entries/base.html" %} #Явно указываем на наш родительский шаблон.
 
{% block content %} #Всё, что находится в этом блоке, может меняться дочерним шаблоном.
 
   {% for entry in entry_list %}
       <article>
           <h2 class="{{ entry.date_created|date:'l' }}">
               {{ entry.date_created|date:'Y-m-d H:i' }}
           </h2>
           <h3>
               <a href="{% url 'entry-detail' entry.id %}">
                   {{ entry.title }}
               </a>
           </h3>
       </article>
   {% endfor %}
{% endblock %} #Не забываем закрыть блок дочернего шаблона.

Изменим и entries/templates/entries/entries_detail.html:

{% extends "entries/base.html" %} #Явно указываем на наш родительский шаблон.
 
{% block content %} #Всё, что находится в этом блоке, может меняться дочерним шаблоном.
 
   <article>
       <h2>{{ entry.date_created|date:'Y-m-d H:i' }}</h2>
       <h3>{{ entry.title }}</h3>
       <p>{{ entry.content }}</p>
   </article>
{% endblock %}

Теперь оба шаблона наследуют HTML-структуру и стиль от родительского шаблона, в том числе стили, указанные в diary.css.

Запустим сервер Django и посмотрим, как изменился наш блог:

Скриншот: Django / Skillbox Media

Теперь блог выглядит интереснее. Однако добавлять, редактировать или удалять записи через панель администратора Django всё-таки неудобно. Реализуем эту функциональность на самом сайте.

Шаг 6


Добавляем кнопки для работы с контентом

В работе с записями в блоге нам нужны четыре основных операции:

  • создать новую запись;
  • прочитать запись;
  • обновить запись;
  • удалить запись.

Все эти действия можно совершить через панель управления, но это неудобно, так как требует открывать дополнительное окно в браузере. Добавим это в веб-интерфейс блога через знакомую нам работу с представлениями, шаблонами и URL.

Добавляем представления

В файле entries/views.py мы уже импортировали ListView и DetailView. Обновим файл, добавив туда новые представления:

from django.urls import reverse_lazy
from django.views.generic import (
   ListView,
   DetailView,
   CreateView, #Импортируем дополнительное представление для создания записей.
   UpdateView, #Импортируем дополнительное представление для обновления записей.
   DeleteView, #Импортируем дополнительное представление для удаления записей.
) 
 
from .models import Entry
 
class EntryListView(ListView):
   model = Entry
   queryset = Entry.objects.all().order_by("-date_created") #Это ключевой запрос — он возвращает все существующие записи по нашему первичному ключу, сортируя их по дате создания.
 
class EntryDetailView(DetailView): #Дополнительно создаём представление с подклассом DetailView. Будем использовать его позже.
   model = Entry
 
class EntryCreateView(CreateView):
   model = Entry
   fields = ["title", "content"] #Указываем на то, какие поля должны будут отображаться в форме при создании новой записи. Дату не указываем, так как она формируется автоматически.
   success_url = reverse_lazy("entry-list")
 
class EntryUpdateView(UpdateView):
   model = Entry
   fields = ["title", "content"] #Указываем на то, какие поля должны будут отображаться в форме при обновлении существующих записей.
 
   def get_success_url(self): #Создаём функцию, которая даёт возможность пользователю остаться на странице с подробным отображением записи после завершения её редактирования.
       return reverse_lazy(
           "entry-detail",
           kwargs={"pk": self.entry.id}
       )
 
class EntryDeleteView(DeleteView):
   model = Entry
   success_url = reverse_lazy("entry-list")

На этот раз недостаточно просто связать классы с нашей моделью Entry. Для EntryCreateView и EntryUpdateView мы явно определили, какие поля модели должны отображаться в форме. А для EntryDeleteView этого не требуется, так как мы планируем удалять запись полностью, вне зависимости от её полей.

Кроме этого, необходимо определить, куда перенаправляется пользователь после отправки формы представления. По умолчанию .get_success_url() просто возвращает значение success_url. В EntryUpdateView мы меняем этот метод, указывая entry.id в качестве аргумента ключевого слова. Это позволяет остаться на странице записи после завершения её редактирования. Вместо использования URL-адресов мы задействуем метод reverse_lazy для обращения к ним по имени, указанному в шаблонах.

Создаём шаблоны для каждой задачи

Как и раньше, Django ищет шаблоны с определённым именем:

  • для EntryCreateView это entry_form.html;
  • для EntryUpdateView это будет entry_update_form.html;
  • для EntryDeleteView это entry_confirm_delete.html.

Если Django не найдёт entry_update_form.html, он может использовать entry_form.html в качестве запасного варианта, восстановив запись в том виде, в каком она существовала до редактирования. Мы можем воспользоваться этой особенностью, создав шаблон, который будет обрабатывать оба варианта в entries/templates/entries/, и добавив для них базовую форму отправки:

{% extends "entries/base.html" %} #Добавляем ссылку на родительский шаблон.
{% block content %}
   <form method="post">
         {% csrf_token %} #Обязательная проверка того, что пользователь сам отправляет форму.
       {{ form.as_p }} 
       <input type="submit" value="Сохранить">
   </form>
   {% if entry %}
       <a href="{% url 'entry-detail' entry.id %}">
           <button>Отмена</button>
       </a>
   {% else %}
       <a href="{% url 'entry-list' %}">
           <button>Отмена</button>
       </a>
   {% endif %}
{% endblock %}

Когда этот шаблон загружается как CreateView для создания новой записи, форма будет пустой. При нажатии на кнопку «Отмена» откроется список со всеми записями. При загрузке в качестве CreateUpdateView для обновления существующей записи она будет показана с существующим заголовком и содержанием записи. Если нажать кнопку «Отмена», откроется страница записи без внесённых изменений.

Существует несколько способов отображения формы в шаблоне. При использовании {{ form.as_p }}, Django отобразит поля, которые мы определили ранее в представлении, обернув их в абзацы. Обязательно добавляем в форму {% csrf_token %}. Это показывает браузеру, что отправку формы запросил сам пользователь.

Создадим entry_confirm_delete.html в entries/templates/entries/:

{% extends "entries/base.html" %} #Добавляем ссылку на родительский шаблон.
{% block content %}
   <form method="post"> {% csrf_token %}
 
 
       <p> #Добавляем текст предупреждения о том, что планируется удаление записи с её названием и датой создания.
           Вы действительно хотите удалить
           <em>"{{ entry.title }}"</em>
           созданную {{ entry.date_created|date:'Y-m-d' }}?
       </p>
       <input type="submit" value="Подтвердить"> 
   </form>
   <a href="{% url 'entry-detail' entry.id %}">
       <button>Отмена</button>
   </a>
{% endblock %}

Указываем URL

После того как мы создали представления и их шаблоны, необходимо указать маршруты для доступа к ним в веб-интерфейсе. Для этого добавим три дополнительных пути к urlpatterns в entries/urls.py:

from django.urls import path
from . import views
 
urlpatterns = [
   path(
       "",
       views.EntryListView.as_view(),
       name="entry-list"
   ),
   path(
       "entry/<int:pk>",
       views.EntryDetailView.as_view(),
       name="entry-detail"
   ),
   path(
       "create",
       views.EntryCreateView.as_view(),
       name="entry-create"
   ),
   path(
       "entry/<int:pk>/update",
       views.EntryUpdateView.as_view(),
       name="entry-update",
   ),
   path(
       "entry/<int:pk>/delete",
       views.EntryDeleteView.as_view(),
       name="entry-delete",
   ),
]

Для entry-create требуется только базовый путь создания новой заметки. Как и для созданной ранее entry-detail, для entry-update и entry-delete необходимо указать первичный ключ, чтобы определить, какая запись должна быть обновлена или удалена.

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

Скриншот: Django / Skillbox Media

Всё работает, но для перехода в формы создания, обновления или удаления записей нет кнопок. Исправим это на следующем шаге.

Шаг 7


Делаем блог удобнее для работы

Чтобы создать, изменить или удалить запись, необходимо запомнить соответствующие URL-адреса и ввести их в адресную строку. Это неудобно. Добавим ссылки на наши представления прямо на страницы блога.

Начнём с ссылки на создание новой записи в шаблоне entries/templates/entries/entry_list.html. Откроем файл и в блок {% block content %} добавим следующий код:

<article>
   <h2 class="mark">{% now "Y-m-d H:i" %}</em></h2>
   <a href="{% url 'entry-create' %}"><button>Добавить новую запись</button></a> #Добавляем кнопку на нашу страницу с созданием новой записи.
</article>

Для того чтобы запись можно было быстро редактировать и удалять, добавим код в шаблон entries/templates/entries/entry_detail.html сразу после </article>:

<p>
       <a href="{% url 'entry-update' entry.id %}">Редактировать</a> #Добавляем ссылку на редактирование записи.
       <a href="{% url 'entry-delete' entry.id %}">Удалить</a> #Добавляем ссылку на удаление записи.
   </p>

Откроем наш блог и проверим, как всё работает:

Скриншот: Django / Skillbox Media

Всё получилось! Кнопки в веб-интерфейсе работают.

Что дальше?

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

Также есть много книг, содержащих разные примеры использования фреймворка. Мы рекомендуем три из них:

  • Django for Beginners Уильяма Винсента;
  • Django for Professionals: Production websites with Python & Django Уильяма Винсента;
  • Django Design Patterns and Best Practices Аруна Равиндрана.
Научитесь: Python-фреймворк Django Узнать больше
Понравилась статья?
Да

Пользуясь нашим сайтом, вы соглашаетесь с тем, что мы используем cookies 🍪

Ссылка скопирована