Миксины в Django 4.1
Мы рассмотрим миксины на примере наших представлений.
Если мы хотим обезопасить наш проект, то мы должны запретить добавление, удаление, и обновление статей гостям. В Django существует ряд стандартных миксинов. Например, LoginRequiredMixin, который дает доступ только авторизированным пользователям к какому-либо действию в представлении.
Давайте дополним ArticleCreateView
modules/blog/views/articles.py
from django.contrib.auth.mixins import LoginRequiredMixin
class ArticleCreateView(LoginRequiredMixin, CreateView):
"""
Представление: создание материалов на сайте
"""
model = Article
template_name = 'modules/blog/articles/article-create.html'
form_class = ArticleCreateForm
login_url = 'home'
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)
Пояснения:
- Мы добавили в наше представление миксин с дополнительным функционалом, который не позволяет гостям добавлять материалы.
- Также у этого миксина есть свойство login_url которое перенаправляет неавторизованных пользователей на заданную страницу, так как мы ещё не сделали страничку авторизации, мы добавим главную страницу сайта.
Изменим urls.py, чтоб все выглядело более правильно:
modules/blog/ulrs.py
from django.urls import path, include
from modules.blog.views import ArticleListView, ArticleDetailView, ArticleByCategoryListView, ArticleCreateView, \
ArticleUpdateView, ArticleDeleteView
urlpatterns = [
path('', ArticleListView.as_view(), name='home'),
path('cat/<int:pk>/<str:slug>/', ArticleByCategoryListView.as_view(), name='article-by-cat'),
path('articles/', include([
path('', ArticleListView.as_view(), name='article-list'),
path('create/', ArticleCreateView.as_view(), name='article-create-view'),
path('<str:slug>/', ArticleDetailView.as_view(), name='article-detail'),
path('<str:slug>/update/', ArticleUpdateView.as_view(), name='article-update-view'),
path('<str:slug>/delete/', ArticleDeleteView.as_view(), name='article-delete-view'),
])),
]
Пояснения:
- Все что касается статей, их детальная информация, создание, обновление и удаление начинается с '/articles/'
- Также я добавил ссылку 'home', с тем же представлением, что и у списка статей.
Разлогинимся с сайта и посмотрим результат миксина:
При переходе на страницу неавторизованным пользователем по адресу http://127.0.0.1:8000/articles/create/ нас перебрасывает на главную, как мы и задали в login_url = 'home'

Давайте сделаем какой-нибудь вывод информации например для успешного обновления статьи... Для этого мы можем использовать миксины уведомлений!
from django.contrib.messages.views import SuccessMessageMixin
class ArticleUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
"""
Представление: обновления материала на сайте
"""
model = Article
template_name = 'modules/blog/articles/article-update.html'
context_object_name = 'article'
form_class = ArticleUpdateForm
login_url = 'home'
success_message = 'Вы успешно обновили статью'
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
Пояснения:
- Мы добавили миксин на вывод сообщения об успешном обновлении статьи. Давайте выведем сообщение в html.
В templates я создам дополнительную папку, назвав ее includes. Эта папка будет использована для незначительных шаблонных подключений. А в ней я создам messages.html
templates/includes/messages.html
{% if messages %}
{% for message in messages %}
<div class="alert alert-{% if message.tags %}{{ message.tags }}{% endif %} alert-dismissible fade show" role="alert">
<i class="fas fa-info-circle"></i> {{message}}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
И в main.html подключим этот компонент:
templates/main.html
<!DOCTYPE html>
<html lang="ru">
<head>
{% load static %}
<meta charset="UTF-8">
<title>{{ title }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- INCLUDE CSS -->
<link href="{% static 'plugins/bootstrap-5.2.0-dist/css/bootstrap.min.css' %}" type="text/css" rel="stylesheet">
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-8">
{% include 'includes/messages.html' %}
{% block content %}
{% endblock %}
{% include 'pagination.html' %}
</div>
<div class="col-md-4">
some...
</div>
</div>
</div>
<script src="{% static 'plugins/bootstrap-5.2.0-dist/js/bootstrap.bundle.min.js' %}"></script>
</body>
</html>
Проверим на деле! Пробуем обновить статью авторизованным пользователем и хотим увидеть сообщение об успехе:

функционал SuccessMessageMixin сработал! Отлично.
А теперь давайте сделаем так, чтоб только автор мог удалять и обновлять свои статьи, при этом, чтоб он был авторизован.
Для этого, я создам папку services с файлом mixins.py в модуле Blog. А теперь создадим такой миксин:
modules/blog/services/mixins.py
from django.contrib.auth.mixins import AccessMixin
class AuthorRequiredMixin(AccessMixin):
"""Verify that the current user is authenticated."""
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return self.handle_no_permission()
if request.user.is_authenticated:
if request.user != self.get_object().author:
self.permission_denied_message = 'Редактировать можно лишь автору данной статьи'
return self.handle_no_permission()
return super().dispatch(request, *args, **kwargs)
И если вы используете структуру папок как у меня, то не забываем про __init__.py
from modules.blog.services.mixins import AuthorRequiredMixin
__all__ = 'AuthorRequiredMixin'
И наконец добавляем наш модернизированный миксин
modules/blog/views/articles.py
from modules.blog.services import AuthorRequiredMixin
class ArticleUpdateView(AuthorRequiredMixin, SuccessMessageMixin, UpdateView):
"""
Представление: обновления материала на сайте
"""
model = Article
template_name = 'modules/blog/articles/article-update.html'
context_object_name = 'article'
form_class = ArticleUpdateForm
login_url = 'home'
success_message = 'Вы успешно обновили статью'
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
Для теста я зарегистрирую другой аккаунт и попробую перейти на страничку обновления статьи с другого аккаунта:
При переходе, я получаю исключение в консоле:
[02/Sep/2022 22:04:58] "GET /articles/testovaya-statya-3-nepublichnaya/ HTTP/1.1" 200 12204
Forbidden (Permission denied): /articles/testovaya-statya-3-nepublichnaya/update/
Traceback (most recent call last):
File "C:\Users\Razilator\Desktop\Courses\App\venv\lib\site-packages\django\core\handlers\exception.py", line 55, in inner
response = get_response(request)
File "C:\Users\Razilator\Desktop\Courses\App\venv\lib\site-packages\django\core\handlers\base.py", line 197, in _get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "C:\Users\Razilator\Desktop\Courses\App\venv\lib\site-packages\django\views\generic\base.py", line 103, in view
return self.dispatch(request, *args, **kwargs)
File "C:\Users\Razilator\Desktop\Courses\App\backend\modules\blog\services\mixins.py", line 14, in dispatch
return self.handle_no_permission()
File "C:\Users\Razilator\Desktop\Courses\App\venv\lib\site-packages\django\contrib\auth\mixins.py", line 48, in handle_no_permission
raise PermissionDenied(self.get_permission_denied_message())
django.core.exceptions.PermissionDenied: Редактировать можно лишь автору данной статьи
[02/Sep/2022 22:05:01] "GET /articles/testovaya-statya-3-nepublichnaya/update/ HTTP/1.1" 403 135
И вот такую надпись: доступ запрещен

Отлично, наш миксин работает. А давайте мы выдадим предупреждение и просто перенаправим пользователя, чтоб он понимал, в чем суть ошибки:
Обновляем наш миксин:
class AuthorRequiredMixin(AccessMixin):
"""Verify that the current user is authenticated."""
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return self.handle_no_permission()
if request.user.is_authenticated:
if request.user != self.get_object().author:
messages.info(request, 'Редактирование и удаление доступно только автору статьи')
return redirect('home')
return super().dispatch(request, *args, **kwargs)
Проверяем:

Отлично. По такому примеру вы можете создавать любые миксины для ваших классов. Подробнее вы можете ознакомиться уже из официальной документации по Using mixins with class-based views | Django documentation | Django