#35. Middleware. Signals. Messages

Middleware. Signals. Messages

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) (якщо описано) для обробки request.
  2. Викликає self.get_response(request), щоб отримати response для подальшого використання.
  3. Викликає self.process_response(request, response) (якщо описано) для обробки response.
  4. Повертає response

Навіщо це потрібно?

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

Наприклад, такий вигляд має 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

Часто ми опиняємося в ситуації, коли нам потрібно виконувати будь-які дії до\після якоїсь певної події, ми звичайно можемо прописати код там, де нам потрібно, але замість цього ми можемо використовувати сигнали.

Сигнали відловлюють, що певна дія виконана або буде наступною, і виконує необхідний код.

Список сигналів тут.
Опис тут.

Приклади сигналів:

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("Запит завершено!")

У сигналу є параметр 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


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

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

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

Інфраструктура повідомлень дає змогу вам тимчасово зберігати повідомлення в одному запиті та витягувати їх для відображення в наступному запиті (зазвичай у наступному).

Кожне повідомлення має певний рівень, який визначає його пріоритет (наприклад, інформація, попередження або помилка).

Підключення

За дефолтом, якщо проєкт був створений через django-admin, то messages від самого початку підключені.

django.contrib.messages мають бути в INSTALLED_APPS.

У змінній MIDDLEWARE мають бути

django.contrib.sessions.middleware.SessionMiddleware
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 %}

Найчастіше такий код розташовують у блоці в базовому шаблоні, щоб не вказувати його на кожній сторінці окремо.

Використання у 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