#35. Middleware. Signals. Messages

Middleware. Signals. Messages

enter image description here

Middleware

Документация тут

Мы с вами рассмотрели основные этапы того, какие этапы должен пройти реквест на всём пути нашей реквест-респонс системы, но на самом деле каждый реквест проходит множество дополнительных обработок, таких как middleware, причём каждый реквест делает это дважды, при “входе” и при “выходе”.

Middleware (переводится как промежуточное программное обеспечение) позволяет обрабатывать запросы из браузера, прежде чем они достигнут представления Django, а также ответы от представлений до того, как они возвращаются в браузер. Django ведет список middleware для каждого проекта.

Если открыть файл settings.py то там можно обнаружить переменную MIDDLEWARE (или MIDDLEWARE_CLASSESдля старых версий Django), которая выглядит примерно вот так:

MIDDLEWARE = [  
    'django.middleware.security.SecurityMiddleware',  
    'django.contrib.sessions.middleware.SessionMiddleware',  
    'django.middleware.common.CommonMiddleware',  
    'django.middleware.csrf.CsrfViewMiddleware',  
    'django.contrib.auth.middleware.AuthenticationMiddleware',  
    'django.contrib.messages.middleware.MessageMiddleware',  
    'django.middleware.clickjacking.XFrameOptionsMiddleware',  
]

Каждая из этих строк, это отдельный класс, и абсолютно каждый реквест проходит через код описанный в этих классах. Например, django.contrib.auth.middleware.AuthenticationMiddleware отвечает за то, чтобы в нашем реквесте всегда был пользователь, если он залогинен, а django.middleware.csrf.CsrfViewMiddleware отвечает за проверку наличия и
правильности CSRF токена, который мы рассматривали ранее.

Middleware применяется в том же порядке, в каком оно добавлено в список в настройках Django. Когда браузер отправляет запрос, он обрабатывается так:

Browser -> M_1 -> M_2 -> ... -> M_N -> View

Представление получает запрос, выполняет некоторые операции и возвращает ответ. На пути к браузеру ответ снова проходить через каждое middleware, но в обратном порядке:

Browser <- M_1 <- M_2 <- ... <- M_N <- View

Middleware это по своей сути это декоратор над реквестом

Как этим пользоваться?

Если мы хотим использовать самописные middleware, мы должны понимать как они работают.
Можно описать middleware двумя способами: функциональным и основанным на классах, рассмотрим оба:

Функциональный:

middleware.py

def simple_middleware(get_response):
    # One-time configuration and initialization.

    def middleware(request):
        # Code to be executed for each request before
        # the view (and later middleware) are called.

        response = get_response(request)

        # Code to be executed for each request/response after
        # the view is called.

        return response

    return middleware

Как вы можете заметить синтаксис очень близок к декораторам.

get_response - функция, которая отвечает за всё что происходит вне middleware и отвечает за обработку запроса, по сути, это будет наша view, а мы можем дописать любой нужный нам код, до или после, соответственно на “входе” реквеста или на “выходе” респонса.

Так почти никто не пишет :) рассмотрим как этот же функционал работает для классов:

middleware.py

class SimpleMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
        # One-time configuration and initialization.

    def __call__(self, request):
        # Code to be executed for each request before
        # the view (and later middleware) are called.

        response = self.get_response(request)

        # Code to be executed for each request/response after
        # the view is called.

        return response

При таком подходе функционал работает при помощи меджик методов, функционально выполняет то же самое.

При инициализации, мы получаем обработчик, а при выполнении вызываем его же, но с возможностью добавить нужный код до или после.

Чтобы активировать middleware нам необходимо дописать путь к нему, в переменную MIDDLEWARE в settings.py.

Допустим, если мы создали файл middlewares.py в приложении под названием main и в этом файле создали класс CheckUserStatus который нужен, чтобы мы могли обработать какой-либо статус пользователя, нужно дописать в переменную этот класс:

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'main.middlewares.CheckUserStatus',  # Новый middleware
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

Обратите внимание я добавил middleware после django.contrib.auth.middleware.AuthenticationMiddleware, так как до этого middleware в нашем реквесте нет переменной юзер.

Несколько middleware

middleware.py

class FirstMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        print('FirstMiddleware in')
        response = self.get_response(request)
        print('FirstMiddleware out')
        return response


class SecondMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        print('SecondMiddleware in')
        response = self.get_response(request)
        print('SecondMiddleware out')
        return response

settings.py

MIDDLEWARE = [  
    'django.middleware.security.SecurityMiddleware',  
    'django.contrib.sessions.middleware.SessionMiddleware',  
    'django.middleware.common.CommonMiddleware',  
    'django.middleware.csrf.CsrfViewMiddleware',  
    'django.contrib.auth.middleware.AuthenticationMiddleware',  
    'django.contrib.messages.middleware.MessageMiddleware',  
    'django.middleware.clickjacking.XFrameOptionsMiddleware',  
    'myapp.middleware.FirstMiddleware',  
    'myapp.middleware.SecondMiddleware',  
]

Вывод в консоль

FirstMiddleware in
SecondMiddleware in
SecondMiddleware out
FirstMiddleware out

Миксин для middleware

На самом деле для написания middleware существует миксин, чтобы упростить наш код.

django.utils.deprecation.MiddlewareMixin

В нём уже расписаны методы __init__ и __call__

__init__ принимает метод для обработки реквеста, а в вызове расписаны методы для обработки реквеста или респонса.

Метод __call__ вызывает 4 действия:

  1. Вызывает self.process_request(request) (Если описан) для обработки ревекста.
  2. Вызывает self.get_response(request) , чтобы получить респонс для дальнейшего использования.
  3. Вызывает self.process_response(request, response) (Если описан) для обработки респонса.
  4. Возвращает респонс

Зачем это нужно?

Чтобы описывать только тот функционал, который мы будем использовать, и случайно не зацепить, что-то рядом.

Например, так выглядит middleware для добавления юзера в реквест.

from django.contrib import auth
from django.utils.deprecation import MiddlewareMixin
from django.utils.functional import SimpleLazyObject


def get_user(request):
    if not hasattr(request, '_cached_user'):
        request._cached_user = auth.get_user(request)
    return request._cached_user


class AuthenticationMiddleware(MiddlewareMixin):
    def process_request(self, request):
        assert hasattr(request, 'session'), (
            "The Django authentication middleware requires session middleware "
            "to be installed. Edit your MIDDLEWARE setting to insert "
            "'django.contrib.sessions.middleware.SessionMiddleware' before "
            "'django.contrib.auth.middleware.AuthenticationMiddleware'."
        )
        request.user = SimpleLazyObject(lambda: get_user(request))

Всё что тут описано, это что делать при реквесте, добавить реквесту юзера, информация о котором как мы помним, хранится в сессии.

Signals

enter image description here
Часто мы оказываемся к ситуации когда нам нужно выполнять какие-либо действия до\после какого-то определённого события, мы конечно можем прописать код там, где нам нужно, но вместо этого мы можем использовать сигналы.

Сигналы отлавливают, что определённое действие выполнено или будет следующим, и выполняет необходимый код.

Список экшенов тут.
Описание тут.

enter image description here

Примеры сигналов:

from django.db.models.signals import (
    pre_save,
    post_save,
    pre_delete,
    post_delete,
    m2m_changed,

)
from django.core.signals import (
    request_started,
    request_finished,
)


pre_save 
post_save # Выполняется перед сохраннием или сразу после сохранения объекта

pre_delete 
post_delete # Выполняется перед удалением или сразу после удаления объекта

m2m_changed # Выполняется при изменении любых мэни ту мени связей (добавили студента в группу или убрали, например)
request_started 
request_finished # Выполняется при начале запроса, или при тего завершении.

Каждый сигнал имеет функции connect и disconnect для того чтобы привязать\отвязать к действию сигнал

from django.core.signals import request_finished

request_finished.connect(my_callback)

где my_callback это функция, которую нужно выполнять по получению сигнала.

Но гораздо чаще применяется синтаксис с использованием декоратора receiver

from django.core.signals import request_finished
from django.dispatch import receiver


@receiver(request_finished)
def my_callback(sender, **kwargs):
    print("Request finished!")

У сигнала есть параметр receiver и может быть параметр sender, сендер, это объект который отправляет сигнал, например модель, для которой описывается сигнал.

from django.db.models.signals import pre_save
from django.dispatch import receiver
from myapp.models import MyModel


@receiver(pre_save, sender=MyModel)
def my_handler(sender, **kwargs):
    ...

Сигнал можно создать под любое действие, если это необходимо. Допустим, нужно отправить сигнал, что пицца готова.

Сначала создадим сигнал.

import django.dispatch

pizza_done = django.dispatch.Signal()

И в нужном месте можно отправить

class PizzaStore:
    ...

    def send_pizza(self, toppings, size):
        pizza_done.send(sender=self.__class__, toppings=toppings, size=size)
        ...

Примеры

signals.py
Удалить связанные объекты

from django.db.models.signals import pre_delete, post_delete  
from django.dispatch import receiver

from .models import ImageModel

def post_delete_unique_code(sender, instance, **kwargs):  
    instance.related_obj.delete()
   
post_delete.connect(post_delete_unique_code, ImageModel)

# OR 

@receiver(pre_delete, sender=ImageModel)  
def pre_delete_image(sender, instance, **kwargs):  
	instance.related_obj.delete()

Messages

enter image description here
Дока тут

Довольно часто в веб-приложениях вам необходимо отображать одноразовое уведомление для пользователя после обработки формы или некоторых других типов пользовательского ввода (“Вы успешно зарегистрировались”, “Скидка активирована”, “Недостаточно бонусов”).

Для этого Django обеспечивает полную поддержку обмена сообщениями на основе файлов cookie и сеансов как для анонимных, так и для аутентифицированных пользователей.

Инфраструктура сообщений позволяет вам временно хранить сообщения в одном запросе и извлекать их для отображения в следующем запросе (обычно в следующем).

Каждое сообщение имеет определенный уровень, который определяет его приоритет (например, информация, предупреждение или ошибка).

Подключение

По дефолту, если проект был создан через django-admin то messages изначально подключены.

django.contrib.messages должны быть в INSTALLED_APPS.

В переменной MIDDLEWARE должны быть django.contrib.sessions.middleware.SessionMiddleware
and django.contrib.messages.middleware.MessageMiddleware.

По дефолту данные сообщений хранятся в сессии, это является причиной почему middleware для сессий должен быть подключен.

Под ключом context_processors в переменной TEMPLATES должны содержаться django.contrib.messages.context_processors.messages

context_processors

Ключ в переменной OPTIONS в переменной TEMPLATES, отвечает за то, что по дефолту будет присутствовать как переменная во всех наших темплейтах, изначально выглядит вот так:

'context_processors': [
    'django.template.context_processors.debug',
    'django.template.context_processors.request',
    'django.contrib.auth.context_processors.auth',
    'django.contrib.messages.context_processors.messages',
]

django.template.context_processors.debug - Если в settings.py переменная DEBUG==True добавляет в темплейт информацию о подробностях, если произошла ошибка

django.template.context_processors.request - Добавляет в контекст данные из реквеста, переменная request.

django.contrib.auth.context_processors.auth - Добавляет переменную user с информацией о пользователе.

django.contrib.messages.context_processors.messages - Добавляет сообщения на страницу.

Storage backends

Хранить сообщения можно в разных местах.

По дефолту существует три варианта хранения:

class storage.session.SessionStorage - Хранение в сессии

class storage.cookie.CookieStorage - Хранение в куки

class storage.fallback.FallbackStorage - Пытаемся хранить в куке, если не помещается используем сессию. Будет использовано, по умолчанию.

Если нужно изменить, добавте в settings.py переменную:

MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage'

Если нужно написать свой класс для хранения сообщений, то нужно наследоваться от class storage.base.BaseStorage и
описать 2 метода _get и _store

Как этим пользоваться

Например, во view, необходимо добавить сообщение.

Это можно сделать несколькими способами:

add_message

from django.contrib import messages

messages.add_message(request, messages.INFO, 'Hello world.')

Метод add_message позволяет добавить сообщение к реквесту, принимает сам реквест, тип сообщения (успех, провал информация итд.), и сам текст сообщения. На самом деле второй параметр это просто цифра, а текст добавлен для чтения.

Чаще всего используется в методах form_valid, form_invalid

Сокращенные методы

messages.debug(request, '%s SQL statements were executed.' % count)
messages.info(request, 'Three credits remain in your account.')
messages.success(request, 'Profile details updated.')
messages.warning(request, 'Your account expires in three days.')
messages.error(request, 'Document deleted.')

Эти 5 типов сообщений являются стандартными, но если необходимо, всегда можно добавить свои типы, как это сделать, описано в доке.

Как отобразить

Контекст процессор, который находится в настройках уже добавляет нам в темплейт переменную messages, а дальше мы можем использовать классические темплейт теги

Примеры:

{% if messages %}
<ul class="messages">
    {% for message in messages %}
    <li
            {% if message.tags %} class="{{ message.tags }}" {% endif %}>{{ message }}
    </li>
    {% endfor %}
</ul>
{% endif %}
{% if messages %}
<ul class="messages">
    {% for message in messages %}
    <li
            {% if message.tags %} class="{{ message.tags }}" {% endif %}>
        {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}Important: {% endif %}
        {{ message }}
    </li>
    {% endfor %}
</ul>
{% endif %}

Чаще всего такой код располагают в блоке в базовом шаблоне, чтобы не указывать его на каждой странице отдельно.
enter image description here

Использование во view

Если нам вдруг необходимо получить список текущих сообщение во view, мы можем это сделать, при помощи метода get_messages

from django.contrib.messages import get_messages

storage = get_messages(request)
for message in storage:
    do_something_with_the_message(message)

Messages и class-base view

Можно добавлять сообщения через миксины, примеры:

from django.contrib.messages.views import SuccessMessageMixin
from django.views.generic.edit import CreateView
from myapp.models import Author


class AuthorCreate(SuccessMessageMixin, CreateView):
    model = Author
    success_url = '/success/'
    success_message = "%(name)s was created successfully"
from django.contrib.messages.views import SuccessMessageMixin
from django.views.generic.edit import CreateView
from myapp.models import ComplicatedModel


class ComplicatedCreate(SuccessMessageMixin, CreateView):
    model = ComplicatedModel
    success_url = '/success/'
    success_message = "%(calculated_field)s was created successfully"

    def get_success_message(self, cleaned_data):
        return self.success_message % dict(
            cleaned_data,
            calculated_field=self.object.calculated_field,
        )

Практика:

  1. Задания с прошлого занятия изменить так, чтобы логика работала не на одной конкретной странице, а вообще на любой.

  2. Для прошлого задания сделать вывод текста не на странице, а при помощи messages

  3. Прошлое задание сделать не для всех страниц, а только для любых двух (на ваш выбор).

Литература (что почитать)

  1. Начало работы с middleware в Django