Django База [2023]: CRUD представления для создания, чтения, обновления и удаления экземпляров модели Django #18
Django

Django База [2023]: CRUD представления для создания, чтения, обновления и удаления экземпляров модели Django #18

Razilator

В этой статье мы рассмотрим, что такое CRUD запросы, как они работают и реализуем их для нашей модели Article в Django, выведем формы в шаблон, настроим urls.py.

Что такое CRUD запросы?

В Django для взаимодействия с базой данных существует концепция ORM.

Понятие ORM означает возможность преобразования и взаимодействия с языком приложения, у нас это Python с базой данных SQL.

В любой реализации ORM есть 4 ключевых понятия, которые добавляют к аббревиатуре CRUD:

  • Create - создать или добавить данные в базу данных;
  • Retrieve - получить и прочитать данные из базы данных;
  • Update - обновление данных в базе данных;
  • Delete - удалить данные.

Проще говоря, ORM — это программа, освобождающая нас от необходимости знать язык запросов - SQL.

Создание формы для представления Create / Update

Первым делом, в приложении blog мы создадим файл forms.py для модели Article (Статьи).

Создаем файл forms.py
Создаем файл forms.py

Добавим следующий фрагмент кода для реализации формы под модель наших статей для добавления и обновления:

modules/blog/forms.py
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.

blog/views.py
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

blog/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 для добавления статьи:

templates/blog/articles_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 '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

На самом деле, в уроках ранее мы уже реализовали эти два представления. Но я покажу ещё раз, как оно выглядит:

blog/views.py
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

blog/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"),
]

Шаблон используемый для детальной статьи:

templates/blog/articles_detail.html
{% 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 %}

Шаблон используемый для списка статей:

templates/blog/articles_list.html
{% 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:

blog/forms.py
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.

Отлично, перейдем к самому представлению:

blog/views.py
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 для обработки представления обновления:

blog/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 со следующим содержимым:

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

blog/views.py
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:

blog/urls.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"),
]

Отлично, теперь реализуем шаблон для удаления статьи.

templates/blog/articles_delete.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 %}

В примере выше мы обращаемся к 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, если вы запутались:

blog/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)

На этом урок закончен. Надеюсь у вас все получилось!

;