CRUD запросы в Django 4.1
Я создам три представления на основе классов для статей, это обновление, добавление и удаление статьи. Вы можете пользоваться моим примером и улучшать его так, как вашей душе угодно.
Напоминаю, что я использую декомпозицию проекта для своего удобства, поэтому я создам папку forms, а в ней __init__.py, с файлом articles.py
Структура выглядит следующим образом:
- modules
- blog
- forms
- __init__.py
- articles.py
- migrations
- models
- views
- forms
- blog
Выглядит это следующим образом:

Кстати, чтоб создавать папку сразу с __init__.py в PyCharm, просто выберете создать Python Package

Создадим форму для создания статей на сайте:
modules/blog/forms/articles.py
from django import forms
from modules.blog.models import Article
class ArticleCreateForm(forms.ModelForm):
"""
Форма добавления статей на сайте
"""
class Meta:
model = Article
fields = (
'title',
'slug',
'category',
'short_description',
'full_description',
'thumbnail',
'meta_title',
'meta_keywords',
'meta_description',
'is_published',
)
def __init__(self, *args, **kwargs):
"""
Обновление стилей формы
"""
super().__init__(*args, **kwargs)
for field in self.fields:
self.fields[field].widget.attrs.update({
'class': 'form-control',
'autocomplete': 'off'
})
self.fields['meta_title'].widget.attrs.update({
'placeholder': 'Введите мета-название для поисковой системы'
})
self.fields['title'].widget.attrs.update({
'placeholder': 'Заголовок статьи'
})
self.fields['slug'].widget.attrs.update({
'placeholder': 'Ссылка статьи (необязательно)'
})
self.fields['meta_description'].widget.attrs.update({
'placeholder': 'Введите небольшое описание в 300 символов для поисковой системы'
})
self.fields['meta_keywords'].widget.attrs.update({
'placeholder': 'Введите ключевые слова через запятую для поиска'
})
self.fields['category'].empty_label = 'Выберите категорию'
self.fields['is_published'].widget.attrs.update({
'class': 'form-check-input'
})
Пояснения:
- Наследуемся от ModelForm.
- В мета прописываем необходимые поля при заполнении статьи с сайта.
- Модель - Article
- Не добавляем created_at, updated_at, т.к они создаются автоматически.
- В методе инициализации def __init__ мы добавляем стили, добавляем placeholder для полей, чтоб было более понятно для пользователей.
- Вместо "-" для категорий мы будем выводить надпись "Выберите категорию"
Если вы используете структуру как у меня, то добавляем в __init__.py следующее:
from modules.blog.forms.articles import ArticleCreateForm
__all__ = '__all__'
Давайте перейдем к созданию представлений.
CreateView в Django 4.1
Импортируем сразу необходимые представления, и создаем ArticleCreateView
modules/blog/views/articles.py
from django.views.generic import CreateView, DeleteView, UpdateView
from modules.blog.forms import ArticleCreateForm
class ArticleCreateView(CreateView):
"""
Представление: создание материалов на сайте
"""
model = Article
template_name = 'modules/blog/articles/article-create.html'
form_class = ArticleCreateForm
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = 'Добавление статьи на сайт'
return context
def form_valid(self, form):
form.instance.author = self.request.user
form.save()
return super().form_valid(form)
Пояснения:
- Импортируем нашу форму
- В валидации формы, мы добавляем автора за текущего пользователя и проверяем ее на ошибки.
- Если ошибок нет, сохраняем форму.
Напоминаю, что если вы проходили все уроки, то на данный момент у вас views/articles.py должен выглядеть так:
from django.views.generic import ListView, DetailView, CreateView, DeleteView, UpdateView
from modules.blog.forms import ArticleCreateForm
from modules.blog.models import Article, Category
class ArticleListView(ListView):
model = Article
template_name = 'modules/blog/articles/article-list.html'
context_object_name = 'articles'
paginate_by = 1
queryset = Article.objects.all().filter(is_published=True)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = 'Главная страница'
return context
class ArticleDetailView(DetailView):
model = Article
template_name = 'modules/blog/articles/article-detail.html'
context_object_name = 'article'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = self.object.title
return context
class ArticleByCategoryListView(ListView):
model = Article
template_name = 'modules/blog/articles/article-list.html'
context_object_name = 'articles'
category = None
def get_queryset(self):
self.category = Category.objects.get(pk=self.kwargs['pk'])
queryset = Article.objects.all().filter(category_id=self.category.id)
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = f'Статьи из категории: {self.category.title}'
return context
class ArticleCreateView(CreateView):
"""
Представление: создание материалов на сайте
"""
model = Article
template_name = 'modules/blog/articles/article-create.html'
form_class = ArticleCreateForm
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = 'Добавление статьи на сайт'
return context
def form_valid(self, form):
form.instance.author = self.request.user
form.save()
return super().form_valid(form)
Шаблон html для создания статьи:
templates/modules/blog/articles/article-create.html
{% extends 'main.html' %}
{% block content %}
<div class="card mb-3 border-0 nth-shadow">
<div class="card-body">
<div class="card-title nth-card-title">
<h4>Добавление статьи</h4>
</div>
<form method="post" action="{% url 'article-create-view' %}" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<div class="d-grid gap-2 d-md-block mt-2">
<button type="submit" class="btn btn-dark">Добавить статью</button>
</div>
</form>
</div>
</div>
{% endblock %}
Пояснения:
- Обязательно добавляем enctype="multipart/form-data" для загрузки изображений/медиа файлов.
- {% csrf_token %} для защиты от подмены данных.
- Метод POST, так как мы добавляем.
- И action="{% url 'article-create-view' %}" в форме указывающий на представление.
- Остальное - Bootstrap стили.
Отлично, осталось добавить в urls.py созданное представление:
modules/blog/urls.py
from django.urls import path
from modules.blog.views import ArticleListView, ArticleDetailView, ArticleByCategoryListView, ArticleCreateView
urlpatterns = [
path('', ArticleListView.as_view(), name='article-list'),
path('<str:slug>/', ArticleDetailView.as_view(), name='article-detail'),
path('cat/<int:pk>/<str:slug>/', ArticleByCategoryListView.as_view(), name='article-by-cat'),
path('articles/create/', ArticleCreateView.as_view(), name='article-create-view')
]
Проверяем на сайте!
Вот так выглядит форма, самая простая, без особых заморочек

После нажатия добавить нас перекидывает на добавленную статью:

А если перейдем в список:

А теперь создадим форму для обновления статьи, а также представление.
ArticleUpdateForm
modules/blog/forms/articles.py
class ArticleUpdateForm(ArticleCreateForm):
class Meta:
model = Article
fields = ArticleCreateForm.Meta.fields + ('reason', 'is_fixed')
def __init__(self, *args, **kwargs):
"""
Обновление стилей формы
"""
super().__init__(*args, **kwargs)
self.fields['is_fixed'].widget.attrs.update({
'class': 'form-check-input'
})
Пояснения:
- В принципе, вы могли использовать форму создания статьи, но так как я хочу добавить кнопку для фиксации статьи и причину обновления, я наследуюсь от нашей формы для создания и добавляю необходимые поля.
- Задаю стиль для чекбокса в стиле Bootstrap.
UpdateView в Django 4.1
Теперь добавим представление обновления.
class ArticleUpdateView(UpdateView):
"""
Представление: обновления материала на сайте
"""
model = Article
template_name = 'modules/blog/articles/article-update.html'
context_object_name = 'article'
form_class = ArticleUpdateForm
def form_valid(self, form):
form.instance.updated_by = self.request.user
form.save()
return super().form_valid(form)
def get_context_data(self, *, object_list=None, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = f'Обновление статьи: {self.object.title}'
return context
Пояснения:
- Как помните, мы в модели указали автора обновления. Я снова получаю его из текущего пользователя и сохраняю форму.
- В контекст я передаю название редактируемой статьи.
Давайте добавим в urls.py ссылку на обновление
modules/blog/urls.py
from django.urls import path
from modules.blog.views import ArticleListView, ArticleDetailView, ArticleByCategoryListView, ArticleCreateView, \
ArticleUpdateView
urlpatterns = [
path('', ArticleListView.as_view(), name='article-list'),
path('<str:slug>/', ArticleDetailView.as_view(), name='article-detail'),
path('cat/<int:pk>/<str:slug>/', ArticleByCategoryListView.as_view(), name='article-by-cat'),
path('articles/create/', ArticleCreateView.as_view(), name='article-create-view'),
path('articles/<str:slug>/update/', ArticleUpdateView.as_view(), name='article-update-view'),
]
Передаем обязательно slug, или же pk. В моем случае я передаю слаг для определения объекта и его редактирования.
Теперь создадим шаблон для обновления:
templates/modules/blog/articles/article-update.html
{% extends 'main.html' %}
{% block content %}
<div class="card mb-3 border-0 nth-shadow">
<div class="card-body">
<div class="card-title nth-card-title">
<h4>Обновление статьи: {{ article.title }}</h4>
</div>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<div class="d-grid gap-2 d-md-block mt-2">
<button type="submit" class="btn btn-dark">Обновить статью</button>
</div>
</form>
</div>
</div>
{% endblock %}
Пояснения:
- Обязательно добавляем enctype="multipart/form-data" для загрузки изображений/медиа файлов.
- {% csrf_token %} для защиты от подмены данных.
- Метод POST, так как мы добавляем/обновляем.
- Остальное - Bootstrap стили.
Проверяем на сайте:

Как видите, добавились поля из новой формы! Давайте попробуем вывести причину обновления и того, кто обновил?
В article-detail.html:
{% extends 'main.html' %}
{% block content %}
<div class="card border-0">
<div class="card-body">
<div class="row">
<div class="col-md-4">
<figure>
<img src="{{ article.get_thumbnail }}" width="200" alt="{{ article.title }}">
</figure>
</div>
<div class="col-md-8">
<h5 class="card-title">
{{ article.title }}
</h5>
<small class="card-subtitle">
{{ article.created_at }} / {{ article.category }}
</small>
<div class="card-text">
{{ article.full_description }}
</div>
<hr/>
{% if article.reason %}{{ article.reason }} / обновил: {{ article.updated_by }}{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}
Добавил следующее:
<hr/>
{% if article.reason %}{{ article.reason }} / обновил: {{ article.updated_by }}{% endif %}
Обновляем с задачей причины, и нажимаем обновление.

Отлично, представление обновления работает. Осталось последнее - представление удаления.
DeleteView в Django 4.1
modules/blog/views/articles.py
from django.urls import reverse_lazy
class ArticleDeleteView(DeleteView):
"""
Представление: удаления материала
"""
model = Article
success_url = reverse_lazy('article-list')
context_object_name = 'article'
template_name = 'modules/blog/articles/article-delete.html'
def get_context_data(self, *, object_list=None, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = f'Удаление статьи: {self.object.title}'
return context
Пояснения:
- В параметре success_url при удалении добавил редирект на страницу со статьями
Добавим представление в urls.py
modules/blog/urls.py
from django.urls import path
from modules.blog.views import ArticleListView, ArticleDetailView, ArticleByCategoryListView, ArticleCreateView, \
ArticleUpdateView, ArticleDeleteView
urlpatterns = [
path('', ArticleListView.as_view(), name='article-list'),
path('<str:slug>/', ArticleDetailView.as_view(), name='article-detail'),
path('cat/<int:pk>/<str:slug>/', ArticleByCategoryListView.as_view(), name='article-by-cat'),
path('articles/create/', ArticleCreateView.as_view(), name='article-create-view'),
path('articles/<str:slug>/update/', ArticleUpdateView.as_view(), name='article-update-view'),
path('articles/<str:slug>/delete/', ArticleDeleteView.as_view(), name='article-delete-view'),
]
Давайте добавим шаблон, и попробуем удалить статью:
templates/modules/blog/articles/article-update.html
{% extends 'main.html' %}
{% block content %}
<div class="card mb-3 border-0 nth-shadow">
<div class="card-body">
<div class="card-title nth-card-title">
<h4>Удаление статьи</h4>
</div>
<form method="post">
{% csrf_token %}
<div class="alert alert-warning" role="alert">
<i class="fas fa-info-circle"></i> Вы собираетесь удалить статью: <strong>{{ article.title }}</strong>, вы подтверждаете свое действие?
</div>
<div class="d-grid gap-2 d-md-block mt-2">
<button type="submit" class="btn btn-dark">Удалить статью</button>
<a href="{{ article.get_absolute_url }}" class="btn btn-primary">Отменить удаление</a>
</div>
</form>
</div>
</div>
{% endblock %}

Жмем удалить и перемещаемся на список статей:

Все, наша статья была успешно удалена. На этом этот урок большой закончен, в следующем уроке мы рассмотрим миксины для безопасности CRUD методов. Надеюсь у вас все получилось и было все более чем понятно :)