#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 дії:
- Викликає
self.process_request(request)(якщо описано) для обробки request. - Викликає
self.get_response(request), щоб отримати response для подальшого використання. - Викликає
self.process_response(request, response)(якщо описано) для обробки response. - Повертає 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,
)
Практика:
- Завдання з минулого заняття змінити так, щоб логіка працювала не на одній конкретній сторінці, а взагалі на будь-якій.
- Для минулого завдання зробити виведення тексту не на сторінці, а за допомогою
messages - Минуле завдання зробити не для всіх сторінок, а тільки для будь-яких двох (на ваш вибір).