Django База [2023]: CRUD представления для создания, чтения, обновления и удаления экземпляров модели Django #18
В этой статье мы рассмотрим, что такое CRUD запросы, как они работают и реализуем их для нашей модели Article в Django, выведем формы в шаблон, настроим urls.py.
Если вы хотите выразить благодарность автору сайта, статей и курса по Django, вы можете сделать это по ссылке ниже:
Что такое CRUD запросы?
В Django для взаимодействия с базой данных существует концепция ORM.
Понятие ORM означает возможность преобразования и взаимодействия с языком приложения, у нас это Python с базой данных SQL.
В любой реализации ORM есть 4 ключевых понятия, которые добавляют к аббревиатуре CRUD:
- Create - создать или добавить данные в базу данных;
- Retrieve - получить и прочитать данные из базы данных;
- Update - обновление данных в базе данных;
- Delete - удалить данные.
Проще говоря, ORM — это программа, освобождающая нас от необходимости знать язык запросов - SQL.
Создание формы для представления Create / Update
Первым делом, в приложении blog мы создадим файл forms.py для модели Article (Статьи).
Добавим следующий фрагмент кода для реализации формы под модель наших статей для добавления и обновления:
from django import forms
from .models import Article
class ArticleCreateForm(forms.ModelForm):
"""
Форма добавления статей на сайте
"""
class Meta:
model = Article
fields = ('title', 'slug', 'category', 'short_description', 'full_description', 'thumbnail', 'status')
def __init__(self, *args, **kwargs):
"""
Обновление стилей формы под Bootstrap
"""
super().__init__(*args, **kwargs)
for field in self.fields:
self.fields[field].widget.attrs.update({
'class': 'form-control',
'autocomplete': 'off'
})
Отлично, форма готова. В примере выше мы наследуемся от класса ModelForm. В классе Meta прописываем необходимые поля при заполнении статьи с сайта.
- Не добавляем
time_create
,time_update
, т.к они создаются автоматически. - В методе инициализации
def __init__()
мы добавляем стили под bootstrap.
Теперь нам необходимо реализовать представления.
Реализация представлений CRUD запросов
Для удобства я рассмотрю каждое представление по отдельности, а в конце покажу получившийся файл views.py, который мы делали из всех уроков, со всеми необходимыми импортами.
Представление для создания статьи - CreateView
Создадим представление для создания в файле views.py нашего приложения blog.
from django.views.generic import CreateView
from .models import Article
from .forms import ArticleCreateForm
class ArticleCreateView(CreateView):
"""
Представление: создание материалов на сайте
"""
model = Article
template_name = 'blog/articles_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)
В примере выше мы наследуемся от класса CreateView. Импортируем нашу модель, а также форму.
- В
get_context_data()
передаем заголовок для<title>
нашего шаблона. - В методе
form_valid()
валидируем нашу форму, а также сохраняем автором текущего пользователя на странице, которого получаем из запросаself.request.user
Добавление обработки в urls.py
from django.urls import path
from .views import ArticleListView, ArticleDetailView, ArticleByCategoryListView, articles_list, ArticleCreateView
urlpatterns = [
path('', ArticleListView.as_view(), name='home'),
path('articles/', articles_list, name='articles_by_page'),
path('articles/create/', ArticleCreateView.as_view(), name='articles_create'),
path('articles/<str:slug>/', ArticleDetailView.as_view(), name='articles_detail'),
path('category/<str:slug>/', ArticleByCategoryListView.as_view(), name="articles_by_category"),
]
Создание шаблона добавления статьи
Используем следующий фрагмент 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 'articles_create' %}" 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 %}
Отлично. Перейдем к другим представлениям.
Представление для чтения детальной статьи DetailView и списка статей ListView
На самом деле, в уроках ранее мы уже реализовали эти два представления. Но я покажу ещё раз, как оно выглядит:
from django.views.generic import DetailView, ListView
from .models import Article
class ArticleDetailView(DetailView):
model = Article
template_name = 'blog/articles_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 ArticleListView(ListView):
model = Article
template_name = 'blog/articles_list.html'
context_object_name = 'articles'
paginate_by = 2
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = 'Главная страница'
return context
В примере выше мы используем реализацию чтения экземпляров из базы данных, как для извлечения одной, так и списка статей.
Обработка в urls.py
from django.urls import path
from .views import ArticleListView, ArticleDetailView, ArticleByCategoryListView, articles_list, ArticleCreateView
urlpatterns = [
path('', ArticleListView.as_view(), name='home'),
path('articles/', articles_list, name='articles_by_page'),
path('articles/create/', ArticleCreateView.as_view(), name='articles_create'),
path('articles/<str:slug>/', ArticleDetailView.as_view(), name='articles_detail'),
path('category/<str:slug>/', ArticleByCategoryListView.as_view(), name="articles_by_category"),
]
Шаблон используемый для детальной статьи:
{% extends 'main.html' %}
{% block content %}
<img src={{ article.thumbnail.url }} alt={{ article.title }} width="250"/>
<strong>{{ article.title }}</strong>
<p>{{ article.full_description }}</p>
<small>{{ article.time_create }}</small>
<hr>
<span>{{ article.category.title }}</span>
{% endblock %}
Шаблон используемый для списка статей:
{% extends 'main.html' %}
{% block content %}
{% for article in articles %}
<div class="card mb-3">
<div class="row">
<div class="col-4">
<img src="{{ article.thumbnail.url }}" class="card-img-top" alt="{{ article.title }}">
</div>
<div class="col-8">
<div class="card-body">
<h5 class="card-title"><a href="{{ article.get_absolute_url }}">{{ article.title }}</a></h5>
<div class="card-subtitle">{{ article.author.username }}</div>
<p class="card-text">{{ article.short_description }}</p>
<a href="{% url 'articles_by_category' article.category.slug %}" class="btn btn-primary">{{ article.category.title }}</a>
</div>
</div>
</div>
</div>
{% endfor %}
{% endblock %}
На этом часть с чтением завершена. Переходим к части с обновлением статьи.
Представление для обновления статьи - UpdateView
Теперь мы рассмотрим класс UpdateView, предназначенный для изменения/обновления текущей статьи в базе данных. Мы создадим новую форму для определения одного поля updater
- автор обновления. В вашем случае, полей может быть больше, например причины обновления.
Создаем форму для обновления статьи в файле forms.py нашего приложения blog:
from django import forms
from .models import Article
class ArticleCreateForm(forms.ModelForm):
"""
Форма добавления статей на сайте
"""
class Meta:
model = Article
fields = ('title', 'slug', 'category', 'short_description', 'full_description', 'thumbnail', 'status')
def __init__(self, *args, **kwargs):
"""
Обновление стилей формы под Bootstrap
"""
super().__init__(*args, **kwargs)
for field in self.fields:
self.fields[field].widget.attrs.update({
'class': 'form-control',
'autocomplete': 'off'
})
class ArticleUpdateForm(ArticleCreateForm):
"""
Форма обновления статьи на сайте
"""
class Meta:
model = Article
fields = ArticleCreateForm.Meta.fields + ('updater', 'fixed')
def __init__(self, *args, **kwargs):
"""
Обновление стилей формы под Bootstrap
"""
super().__init__(*args, **kwargs)
self.fields['fixed'].widget.attrs.update({
'class': 'form-check-input'
})
Конечно, вы могли использовать форму создания статьи для обновления, но для примера я добавил кнопку фиксации и выбора автора обновления статьи.
- Наследовался я уже от существующей формы ArticleCreateForm.
- Подправил стиль для чек-бокса в форме для поля
fixed
.
Отлично, перейдем к самому представлению:
from django.views.generic import UpdateView
from .models import Article
from .forms import ArticleUpdateForm
class ArticleUpdateView(UpdateView):
"""
Представление: обновления материала на сайте
"""
model = Article
template_name = 'blog/articles_update.html'
context_object_name = 'article'
form_class = ArticleUpdateForm
def get_context_data(self, *, object_list=None, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = f'Обновление статьи: {self.object.title}'
return context
def form_valid(self, form):
# form.instance.updater = self.request.user
form.save()
return super().form_valid(form)
В данном коде я отметил 21
строку, как возможность получения пользователя для обновления. Логичнее будет использовать так. Но для примера с доп.полями для формы показываю именно так, чтоб больше знали возможностей реализации.
Обновляем urls.py для обработки представления обновления:
from django.urls import path
from .views import ArticleListView, ArticleDetailView, ArticleByCategoryListView, articles_list, ArticleCreateView, ArticleUpdateView
urlpatterns = [
path('', ArticleListView.as_view(), name='home'),
path('articles/', articles_list, name='articles_by_page'),
path('articles/create/', ArticleCreateView.as_view(), name='articles_create'),
path('articles/<str:slug>/update/', ArticleUpdateView.as_view(), name='articles_update'),
path('articles/<str:slug>/', ArticleDetailView.as_view(), name='articles_detail'),
path('category/<str:slug>/', ArticleByCategoryListView.as_view(), name="articles_by_category"),
]
Теперь необходимо добавить шаблон для формы обновления, поэтому создаем HTML файл в templates/blog - articles_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 %}
Отлично, и подходим к последнему представлению - удаление статьи.
Представление удаления статьи - DeleteView
from django.views.generic import DeleteView
from django.urls import reverse_lazy
from .models import Article
class ArticleDeleteView(DeleteView):
"""
Представление: удаления материала
"""
model = Article
success_url = reverse_lazy('home')
context_object_name = 'article'
template_name = 'blog/articles_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
мы использовали переход после удаления статьи на страницу со всеми статьями с помощью функции reverse_lazy()
.
Добавим обработку в ulrs.py:
from django.urls import path
from .views import ArticleListView, ArticleDetailView, ArticleByCategoryListView, articles_list, ArticleCreateView, ArticleUpdateView, ArticleDeleteView
urlpatterns = [
path('', ArticleListView.as_view(), name='home'),
path('articles/', articles_list, name='articles_by_page'),
path('articles/create/', ArticleCreateView.as_view(), name='articles_create'),
path('articles/<str:slug>/update/', ArticleUpdateView.as_view(), name='articles_update'),
path('articles/<str:slug>/delete/', ArticleDeleteView.as_view(), name='articles_delete'),
path('articles/<str:slug>/', ArticleDetailView.as_view(), name='articles_detail'),
path('category/<str:slug>/', ArticleByCategoryListView.as_view(), name="articles_by_category"),
]
Отлично, теперь реализуем шаблон для удаления статьи.
{% 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 %}
В примере выше мы обращаемся к article
для отмены удаления, так как задали в представлении свойство context_object_name
.
Отлично, все настроено. Осталось все это проверить на работоспособность.
Проверяем возможность добавления статьи, перейдя на следующую страницу: http://127.0.0.1:8000/articles/create/ и попробуем добавить статью.
Статью добавили, все работает так, как нам необходимо.
Проверим возможность обновления, с той же статьей, перейдя на следующую страницу: http://127.0.0.1:8000/articles/test-stati-dobavlennoj-iz-formyi-na-sajte/update/
Отлично, видим дополнительное поле, а также возможность зафиксировать. Давайте изменим изображение этой статьи.
Метод обновления также отлично работает.
Теперь удалим эту статью, перейдя на следующую страницу: http://127.0.0.1:8000/articles/test-stati-dobavlennoj-iz-formyi-na-sajte/delete/
Если мы нажмем отмену удаления, вернемся в эту же статью. Если же нажмем удалить, то вернемся к списку статей. Давайте удалим.
Все, отлично. Все методы работают, и как обещал, прикладываю весь код из views.py, если вы запутались:
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView
from django.urls import reverse_lazy
from .models import Article, Category
from .forms import ArticleCreateForm, ArticleUpdateForm
class ArticleListView(ListView):
model = Article
template_name = 'blog/articles_list.html'
context_object_name = 'articles'
paginate_by = 2
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = 'Главная страница'
return context
class ArticleDetailView(DetailView):
model = Article
template_name = 'blog/articles_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 = 'blog/articles_list.html'
context_object_name = 'articles'
category = None
def get_queryset(self):
self.category = Category.objects.get(slug=self.kwargs['slug'])
queryset = Article.objects.all().filter(category__slug=self.category.slug)
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 = 'blog/articles_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)
class ArticleUpdateView(UpdateView):
"""
Представление: обновления материала на сайте
"""
model = Article
template_name = 'blog/articles_update.html'
context_object_name = 'article'
form_class = ArticleUpdateForm
def get_context_data(self, *, object_list=None, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = f'Обновление статьи: {self.object.title}'
return context
def form_valid(self, form):
# form.instance.updater = self.request.user
form.save()
return super().form_valid(form)
class ArticleDeleteView(DeleteView):
"""
Представление: удаления материала
"""
model = Article
success_url = reverse_lazy('home')
context_object_name = 'article'
template_name = 'blog/articles_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
from django.shortcuts import render
from django.core.paginator import Paginator
def articles_list(request):
articles = Article.objects.all()
paginator = Paginator(articles, per_page=2)
page_number = request.GET.get('page')
page_object = paginator.get_page(page_number)
context = {'page_obj': page_object}
return render(request, 'blog/articles_func_list.html', context)
На этом урок закончен. Надеюсь у вас все получилось!