В данном уроке мы продолжим начатую ранее работу по созданию блога на Django. Теперь речь пойдет о добавлении специальных форм для создания, редактирования и удаления записей на блоге.
Содержание статьи
- CreateView форма для создания записи на блоге
- Ошибка ImproperlyConfigured
- Форма для обновления данных UpdateView
- Создаем страницу удаления записи DeleteView
- Тестируем добавление, обновление и удаление записей через TestCase
CreateView форма для создания записи на блоге
Формы являются очень важными и в то же время весьма сложными в плане реализации элементами. Во время приема вводных данных от пользователя под удар попадает система безопасности (XSS Attacks), при этом нужно создать продвинутый обработчик ошибок, а также возникает необходимость грамотно настроенного интерфейса UI, который предупреждает пользователей о проблемах с формами. Плюс ко всему здесь требуется добиться успешных перенаправлений на нужную страницу.
К счастью, в Django есть встроенные формы, которые значительно облегчают процесс разработки, предоставляя богатый ассортимент инструментов для создания веб-сайта.
Для начала обновим базовый шаблон для отображения прямой ссылки на страницу создания новой записи блога. Код будет выглядеть, как <a href="{% url 'post_new' %}"></a>
, где post_new
является названием нашего URL.
Есть вопросы по Python?
На нашем форуме вы можете задать любой вопрос и получить ответ от всего нашего сообщества!
Паблик VK
Одно из самых больших сообществ по Python в социальной сети ВК. Видео уроки и книги для вас!
Обновленный файл templates/base.html
должен выглядеть следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<!-- templates/base.html --> {% load static %} <html> <head> <title>Django blog</title> <link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400" rel="stylesheet"> <link href="{% static 'css/base.css' %}" rel="stylesheet"> </head> <body> <div> <header> <div class="nav-left"> <h1><a href="{% url 'home' %}">Django blog</a></h1> </div> <div class="nav-right"> <a href="{% url 'post_new' %}">+ New Blog Post</a> </div> </header> {% block content %} {% endblock content %} </div> </body> </html> |
Теперь добавим новый URL-маршрут для страницы создания новой статьи post_new
. В верхней части импортируем пока еще не созданное представление под названием BlogCreateView
. После этого создаем URL маршрут, с названием post_new
, который будет начинаться с post/new/
.
1 2 3 4 5 6 7 8 9 10 |
# blog/urls.py from django.urls import path from .views import BlogListView, BlogDetailView, BlogCreateView # new urlpatterns = [ path('post/new/', BlogCreateView.as_view(), name='post_new'), # new path('post/<int:pk>/', BlogDetailView.as_view(), name='post_detail'), path('', BlogListView.as_view(), name='home'), ] |
Не так уж сложно, верно? Такие же url, представления и шаблоны мы видели ранее.
Теперь перейдем к созданию представления, для чего в верхней части импортируем общий класс представлений под названием CreateView
, наследуем его в классе BlogCreateView
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
# blog/views.py from django.views.generic import ListView, DetailView from django.views.generic.edit import CreateView # новое изменение from .models import Post class BlogListView(ListView): model = Post template_name = 'home.html' class BlogDetailView(DetailView): model = Post template_name = 'post_detail.html' class BlogCreateView(CreateView): # новое изменение model = Post template_name = 'post_new.html' fields = ['title', 'author', 'body'] |
Внутри BlogCreateView
указываем на модель Post из прошлого урока и название нашего HTML шаблона post_new.html
. Для fields
напрямую выбираем поля из таблицы, которые нужно задействовать — в данном случае это title
, author
и body
.
На последнем этапе создаем HTML шаблон, который назовем post_new.html
.
1 |
(blog) $ touch templates/post_new.html |
Затем добавляем следующий HTML код:
1 2 3 4 5 6 7 8 9 10 |
<!-- templates/post_new.html --> {% extends 'base.html' %} {% block content %} <h1>New post</h1> <form action="" method="post">{% csrf_token %} {{ form.as_p }} <input type="submit" value="Save" /> </form> {% endblock content %} |
Разберем выполненные выше действия:
- На верхней строке указан базовый шаблон структуру которого мы будем дополнять;
- Используются HTML тег
<form>
с методом POST, так как мы высылаем данные. Если мы получаем данные из поля поиска, тогда мы бы использовали GET; - Добавляется специальный токен {% csrf_token %}, через который Django обеспечивает защиту от аттак межсайтового скриптинга. Нужно использовать его для всех форм в Django;
- Для отображения формы используется
{{ form.as_p }}
, что помещает форму внутри тега<p>
; - Под конец создается кнопка «Save» для сохранения изменений из формы.
Для просмотра нашей работы запустите веб-сервер через команду python manage.py runserver
и перейдите на страницу http://127.0.0.1:8000/.
Нажмите на ссылку «+ New Blog Post», которая перенаправит вас на http://127.0.0.1:8000/post/new/.
Страница добавления новой записи в блоге
Попробуйте создать новую запись на блоге, после чего подтвердите действие нажав на кнопку «Save».
Ошибка ImproperlyConfigured в Django
Упс! Что же произошло?
В Django сообщения об ошибках могут стать весьма полезными. В данном случае система требует уточнения места, куда будет перенаправлен пользователь после подтверждения формы. Давайте отправим его на индивидуальную страницу записи, таким образом мы сможем сразу посмотреть на созданную запись.
Можно согласиться на предложение Django и добавить к нашей модели новый метод get_absolute_url. Это хорошая практика, которой будет не лишним следовать. Здесь устанавливается прямой URL для объекта, так что даже при возможном изменении структуры URL в будущем, ссылка на некий объект останется прежней. Короче говоря, вам нужно создать методы get_absolute_url()
и __str__()
для каждой модели.
Откройте файл models.py
. Добавьте на второй строке импорт для reverse и новый метод get_absolute_url
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
# blog/models.py from django.db import models from django.urls import reverse # Новый импорт class Post(models.Model): title = models.CharField(max_length=200) author = models.ForeignKey( 'auth.User', on_delete=models.CASCADE, ) body = models.TextField() def __str__(self): return self.title def get_absolute_url(self): # Тут мы создали новый метод return reverse('post_detail', args=[str(self.id)]) |
Reverse является довольно полезной вспомогательной функцией Django, что позволяет отсылаться к определенному объекту через название URL маршрута, в данном случае это post_detail
. URL маршрут будет следующим:
1 |
path('post/<int:pk>/', BlogDetailView.as_view(), name='post_detail'), |
Это означает, что для работы маршрута также нужно передать аргумент pk
или первичный ключ (ID) объекта. Путаницу вызывает тот факт, что в Django pk
и id
взаимозаменяемы, хотя в документации по Django рекомендуется использовать self.id
с get_absolute_url
. Итак, мы сообщаем Django, что конечным местоположением записи будет представление post_detail
, которое находится по URL маршруту posts/<int:pk>/
, следовательно, URL-адрес для первой созданной нами записи будет posts/1
.
Попробуем заново создать запись на блоге, переходим на страницу http://127.0.0.1:8000/post/new/.
После нажатия кнопки «Save» вы будете перенаправлены на индивидуальную страницу созданной записи.
Индивидуальная страница записи
Вернувшись на домашнюю страницу http://127.0.0.1:8000/, вы заметите, что наша предыдущая запись также в списке. Она была успешно создана в базу данных, однако Django не знал, куда после этого перенаправить пользователя.
Домашняя страница блога с четырьмя записями
Конечно, можно зайти в админку Django и удалить нежелательные записи оттуда, но будет лучше добавить формы, при помощи которых пользователь сможет обновлять или удалять записи прямо на сайте блога.
Форма для обновления данных UpdateView в Django
Процесс создания формы для редактирования, при помощи которой пользователи смогут редактировать записи, может показаться знакомым. Мы вновь используем встроенные в Django общие классовые представления UpdateView и создадим HTML шаблон, url маршрут и представление (views).
Для начала добавим новую ссылку в post_detail.html
, таким образом ссылка на редактирование записи можно будет найти на индивидуальной странице самой записи.
1 2 3 4 5 6 7 8 9 10 11 |
<!-- templates/post_detail.html --> {% extends 'base.html' %} {% block content %} <div class='post-entry'> <h2>{{ post.title }}</h2> <p>{{ post.body }}</p> </div> <a href='{% url 'post_edit' post.pk %}'>+ Edit Blog Post</a> {% endblock content %} |
Мы добавили ссылку, используя HTML тег <a href>...</a>
, и тег языка шаблонов {% url ... %}
. Внутри мы уточнили структуру url, которое будет называться post_edit
, а также передали запрашиваемый параметр, которым является первичный ключ записи post.pk
.
Далее создается HTML шаблон для отдельной страницы редактирования данных, которая будет называться post_edit.html
.
1 |
(blog) $ touch templates/post_edit.html |
Добавляем следующий код:
1 2 3 4 5 6 7 8 9 10 |
<!-- templates/post_edit.html --> {% extends 'base.html' %} {% block content %} <h1>Edit post</h1> <form action="" method="post">{% csrf_token %} {{ form.as_p }} <input type="submit" value="Update" /> </form> {% endblock content %} |
Мы вновь используем теги <form></form>
, для обеспечения безопасности добавим csrf_token
от Django, form.as_p
для отображения полей формы и, конечно же, создаем кнопку «Update» для подтверждения изменений.
Теперь перейдем к представлению. На второй строке сверху импортируется общий класс представления UpdateView
, после чего наследуем его в BlogUpdateView
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
# blog/views.py from django.views.generic import ListView, DetailView from django.views.generic.edit import CreateView, UpdateView # Новый импорт класса from .models import Post class BlogListView(ListView): model = Post template_name = 'home.html' class BlogDetailView(DetailView): model = Post template_name = 'post_detail.html' class BlogCreateView(CreateView): model = Post template_name = 'post_new.html' fields = ['title', 'author', 'body'] class BlogUpdateView(UpdateView): # Новый класс model = Post template_name = 'post_edit.html' fields = ['title', 'body'] |
Обратите внимание, что в BlogUpdateView
мы перечисляем по отдельности нужные поля ['title', 'body']
вместо использования '__all__'
. Это делается из-за того, что предполагается, будто автор записи не изменится, а редактироваться будет только заголовок и содержимое поста.
На финальном этапе обновляется файл urls.py
. В верхней части добавляется класс BlogUpdateView
, а затем новый URL маршрут.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# blog/urls.py from django.urls import path from .views import ( BlogListView, BlogDetailView, BlogCreateView, BlogUpdateView, # новое добавление ) urlpatterns = [ path('post/<int:pk>/edit/', BlogUpdateView.as_view(), name='post_edit'), # Новый маршрут path('post/new/', BlogCreateView.as_view(), name='post_new'), path('post/<int:pk>/', BlogDetailView.as_view(), name='post_detail'), path('', BlogListView.as_view(), name='home'), ] |
В верхней части мы добавляем представление BlogUpdateView
к списку импортированных представлений, затем создаем новый url маршрут /post/pk/edit
и называем его post_edit
.
Теперь при переходе на страницу записи, мы увидим ссылку на страницу редактирования данной статьи «+ Edit Blog Post«.
Страница блога с кнопкой редактирования
При нажатии на «+ Edit Blog Post» вы будете перенаправлены на страницу http://127.0.0.1:8000/post/1/edit/ в том случае, если это была ваша первая запись в блоге, т.к. ID = 1.
Страница редактирования записи
Обратите внимание, что форма заполнена уже существующей в базе данных информацией. Изменим ее…
Страница редактирования записи блога
При нажатии кнопки «Update» происходит перенаправление на страницу самой записи, которая уже изменена. Это происходит благодаря настройке метода get_absolute_url. Перейдя на домашнюю страницу сайту, можно увидеть обновленные записи.
Домашняя страница с отредактированной записью
Создаем страницу удаления записи DeleteView
Процесс создания формы для удаления записи очень похож на то, что мы делали с формой обновления записи. Сейчас приступим к созданию представления, url маршрутов и HTML-шаблона. На данном этапе мы будет использовать общее классовое представление DeleteView.
Начнем с добавления ссылки для удаления записи, поместим ее на индивидуальной странице поста post_detail.html
.
1 2 3 4 5 6 7 8 9 10 11 12 |
<!-- templates/post_detail.html --> {% extends 'base.html' %} {% block content %} <div class="post-entry"> <h2>{{ post.title }}</h2> <p>{{ post.body }}</p> </div> <p><a href="{% url 'post_edit' post.pk %}">+ Edit Blog Post</a></p> <p><a href="{% url 'post_delete' post.pk %}">+ Delete Blog Post</a></p> {% endblock content %} |
Далее создаем новый HTML файл для шаблона страницы подтверждения удаления записи. Закрываем локальный веб-сервер через комбинацию CTRL+C
и затем набираем следующую команду:
1 |
(blog) $ touch templates/post_delete.html |
Заполняем файл следующим кодом:
1 2 3 4 5 6 7 8 9 10 |
<!-- templates/post_delete.html --> {% extends 'base.html' %} {% block content %} <h1>Delete post</h1> <form action="" method="post">{% csrf_token %} <p>Are you sure you want to delete "{{ post.title }}"?</p> <input type="submit" value="Confirm" /> </form> {% endblock content %} |
Обратите внимание, что для отображения заголовка записей блога здесь используется post.title
. Можно использовать и object.title
, который также поставляется в DetailView
.
Далее обновляем файл views.py
, импортируя DeleteView
и reverse_lazy
в верхней части, после чего создаем новый класс (представление), которое становится наследником от DeleteView
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
# blog/views.py from django.views.generic import ListView, DetailView from django.views.generic.edit import CreateView, UpdateView, DeleteView # новое from django.urls import reverse_lazy # импортируем новые методы from .models import Post class BlogListView(ListView): model = Post template_name = 'home.html' class BlogDetailView(DetailView): model = Post template_name = 'post_detail.html' class BlogCreateView(CreateView): model = Post template_name = 'post_new.html' fields = ['title', 'author', 'body'] class BlogUpdateView(UpdateView): model = Post template_name = 'post_edit.html' fields = ['title', 'body'] class BlogDeleteView(DeleteView): # Создание нового класса model = Post template_name = 'post_delete.html' success_url = reverse_lazy('home') |
Вместо reverse здесь используется reverse_lazy, таким образом пользователь не будет перенаправлен до тех пор, пока представление не завершит удаление записи из базы данных.
И теперь можно создать новый URL маршрут при помощи импорта представления BlogDeleteView
и добавлении следующих изменений:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
# blog/urls.py from django.urls import path from .views import ( BlogListView, BlogUpdateView, BlogDetailView, BlogCreateView, BlogDeleteView, # Импортируем представление ) urlpatterns = [ path('post/<int:pk>/delete/', # Создаем новый маршрут BlogDeleteView.as_view(), name='post_delete'), path('post/new/', BlogCreateView.as_view(), name='post_new'), path('post/<int:pk>/', BlogDetailView.as_view(), name='post_detail'), path('post/<int:pk>/edit/', BlogUpdateView.as_view(), name='post_edit'), path('', BlogListView.as_view(), name='home'), ] |
Если вновь запустить веб-сервер через python manage.py runserver
и обновить страницу записи, можно увидеть новую ссылку «Delete Blog Post«.
При нажатии на ссылку, пользователь перенаправляется на страницу подтверждения удаления записи, на которой находится только название записи и кнопка подтверждения действий «Confirm«.
Страница для удаления записи
При нажатии кнопки «Confirm» пользователь будет перенаправлен на домашнюю страницу блога. В списке на главной странице больше не будет удаленная нами запись.
Итак, все работает!
Тестируем добавление, обновление и удаление записей через TestCase
Пришло время для тестов — убедимся, что все работает должным образом. К нашей модели мы добавили метод get_absolute_url
, а также новые представления для создания, обновления и удаления записей. Это значит, что нам понадобится четыре новых теста:
def test_get_absolute_url
;def test_post_create_view
;def test_post_update_view
;def test_post_delete_view
.
Обновляем существующий файл tests.py
следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
# blog/tests.py from django.contrib.auth import get_user_model from django.test import TestCase from django.urls import reverse from .models import Post class BlogTests(TestCase): def setUp(self): self.user = get_user_model().objects.create_user( username='testuser', email='test@email.com', password='secret' ) self.post = Post.objects.create( title='A good title', body='Nice body content', author=self.user, ) def test_string_representation(self): post = Post(title='A sample title') self.assertEqual(str(post), post.title) def test_get_absolute_url(self): # new self.assertEqual(self.post.get_absolute_url(), '/post/1/') def test_post_content(self): self.assertEqual(f'{self.post.title}', 'A good title') self.assertEqual(f'{self.post.author}', 'testuser') self.assertEqual(f'{self.post.body}', 'Nice body content') def test_post_list_view(self): response = self.client.get(reverse('home')) self.assertEqual(response.status_code, 200) self.assertContains(response, 'Nice body content') self.assertTemplateUsed(response, 'home.html') def test_post_detail_view(self): response = self.client.get('/post/1/') no_response = self.client.get('/post/100000/') self.assertEqual(response.status_code, 200) self.assertEqual(no_response.status_code, 404) self.assertContains(response, 'A good title') self.assertTemplateUsed(response, 'post_detail.html') def test_post_create_view(self): # new response = self.client.post(reverse('post_new'), { 'title': 'New title', 'body': 'New text', 'author': self.user, }) self.assertEqual(response.status_code, 200) self.assertContains(response, 'New title') self.assertContains(response, 'New text') def test_post_update_view(self): # новое response = self.client.post(reverse('post_edit', args='1'), { 'title': 'Updated title', 'body': 'Updated text', }) self.assertEqual(response.status_code, 302) def test_post_delete_view(self): # новое response = self.client.post( reverse('post_delete', args='1')) self.assertEqual(response.status_code, 302) |
Предполагается, что URL для нашего теста — post/1/
. Здесь только одна запись, и 1
— ее первичный ключ, который Django добавляет автоматически. Чтобы протестировать представление для создания (create), создается новый запись. Затем нужно убедиться, что ответ действительно получен (Код HTTP состояния 200) и содержит новый заголовок и тело статьи с текстом. Чтобы протестировать представление для обновления записи (update), требуется получить доступ к первой записи, у которой pk
равен 1
, который передается как единственный аргумент.
Затем подтверждается факт перенаправления с HTTP кодом 302. Под конец тестируем представление для удаления записи (delete). После удаления записи должен появиться HTTP код состояния 302, и произведено перенаправление на главную страницу, так как удаленного элемента больше не существует.
Упомянутые выше тесты охватывают все возможности нашего блога на Django. Однако всегда есть возможность добавить новые возможности на сайте. Запустите данные тесты сейчас — они все должны пройти.
1 |
(blog) $ python manage.py test |
Заключение
При помощи относительно небольшого количества кода нам удалось создать блог на Django, который позволяет создавать, читать, обновлять и удалять записи. Данная функциональность известна под аббревиатурой CRUD: Create-Read-Update-Delete. Существует несколько способов достижения того же результата. Можно использовать представления на основе функций или же написать свои собственные представления на основе классов. По ходу разбора примеров было продемонстрировано, что для осуществления запрашиваемых операции в Django требуется совсем немного кода.
Обратите внимание на потенциальные проблемы с безопасностью: в настоящее время не только создатель, но любой пользователь может обновлять или удалять записи в блоге! Такой проект идеальным не назовешь. По этой причине Django поставляется со встроенными функциями для ограничения доступа на основе разрешений, которые мы подробно рассмотрим в будущих уроках.
Наш блог на Django не идеален, но все работает. В следующем уроке мы добавим пользовательские аккаунты, а также функциональность для входа, выхода и регистрации.
Являюсь администратором нескольких порталов по обучению языков программирования Python, Golang и Kotlin. В составе небольшой команды единомышленников, мы занимаемся популяризацией языков программирования на русскоязычную аудиторию. Большая часть статей была адаптирована нами на русский язык и распространяется бесплатно.
E-mail: vasile.buldumac@ati.utm.md
Образование
Universitatea Tehnică a Moldovei (utm.md)
- 2014 — 2018 Технический Университет Молдовы, ИТ-Инженер. Тема дипломной работы «Автоматизация покупки и продажи криптовалюты используя технический анализ»
- 2018 — 2020 Технический Университет Молдовы, Магистр, Магистерская диссертация «Идентификация человека в киберпространстве по фотографии лица»