Машинный перевод под контролем человека
Этот контент переведён с помощью машинной генерации, направляемой составленными людьми глоссариями и руководствами по стилю. Поскольку текст не проверяется вручную построчно, возможны отдельные ошибки или неестественные формулировки.
В случае любых расхождений авторитетным источником считается оригинальная версия на английском языке.
События
Метод-хук вроде before_create выполняется только в том представлении (view), где он определён. Система событий позволяет коду за пределами этого представления реагировать на происходящее внутри него: журнал аудита, вебхук или инвалидация кэша могут находиться в одном месте вместо того, чтобы копироваться в каждый создаваемый вами ModelView.
from starlette_admin.events import AdminEvent, AfterCreateContext
async def notify_slack(ctx: AfterCreateContext) -> None:
print(f"New {ctx.view_key} created: pk={ctx.pk}")
admin.events.on(AdminEvent.AFTER_CREATE, notify_slack)
Зарегистрируйте это один раз рядом с экземпляром admin, и endpoint создания каждого представления будет вызывать обработчик, включая представления, которые вы добавите позже.
Уровень представления и уровень admin
У каждого представления есть атрибут events, на который можно подписаться напрямую; область действия такой подписки ограничена этим представлением. У экземпляра Admin атрибут events тоже есть — он охватывает все зарегистрированные в нём представления или их подмножество, если передать keys=.
view.events.on(...): срабатывает только для этого представления.admin.events.on(...): срабатывает для всех текущих и будущих представлений, если не ограничить подписку черезkeys=.
Регистрация на admin.events возможна как до, так и после вызова admin.add_view(...). Порядок не имеет значения: обработчик, зарегистрированный первым, всё равно привяжется к представлению после его добавления.
Методы-хуки и подписки на события
И те, и другие срабатывают в одной и той же точке жизненного цикла запроса. Различие заключается в том, где находится код и скольким представлениям он доступен.
| Характеристика | Метод-хук (before_create, ...) |
Подписка на событие (view.events / admin.events) |
|---|---|---|
| Где находится код | Внутри класса представления | Где угодно, например функция уровня модуля или класс-подписчик |
| Область действия | Конкретное представление | Одно представление (view.events) или все представления (admin.events) |
| Подходит для | Логики, специфичной для данного ресурса (slugify заголовка, проставление метки времени) | Сквозной функциональности (журналы аудита, уведомления, плагины) |
| Можно ли несколько? | Нет, один метод на представление | Да, любое количество обработчиков на событие, упорядоченных по приоритету |
Используйте метод-хук, когда логика неотъемлема от модели. Используйте подписку на событие, когда она не относится к какому-то одному представлению или когда вы поставляете её как переиспользуемый компонент для нескольких админ-панелей.
from typing import Any
from starlette.requests import Request
from starlette_admin.contrib.sqla import ModelView
class PostView(ModelView):
# Относится только к этому представлению, остаётся здесь
async def before_create(
self, request: Request, data: dict[str, Any], obj: Any
) -> None:
obj.slug = data["title"].lower().replace(" ", "-")
Значения AdminEvent
AdminEvent — это строковый enum. Ниже перечислены члены, активно генерируемые жизненным циклом представлений:
| Событие | Когда срабатывает | Класс контекста |
|---|---|---|
BEFORE_CREATE / AFTER_CREATE |
Запись создана | BeforeCreateContext / AfterCreateContext |
AFTER_CREATE_COMMITTED |
Транзакция создания зафиксирована | AfterCreateContext |
BEFORE_EDIT / AFTER_EDIT |
Запись обновлена | BeforeEditContext / AfterEditContext |
AFTER_EDIT_COMMITTED |
Транзакция редактирования зафиксирована | AfterEditContext |
BEFORE_DELETE / AFTER_DELETE |
Запись удалена | BeforeDeleteContext / AfterDeleteContext |
AFTER_DELETE_COMMITTED |
Транзакция удаления зафиксирована | AfterDeleteContext |
BEFORE_ACTION / AFTER_ACTION |
Выполнено пакетное или строчное действие | BeforeActionContext / AfterActionContext |
BEFORE_EXPORT / AFTER_EXPORT |
Запущен экспорт | BeforeExportContext / AfterExportContext |
BEFORE_IMPORT / AFTER_IMPORT |
Запущен импорт | BeforeImportContext / AfterImportContext |
AFTER_LOGIN |
Вход выполнен успешно | AfterLoginContext |
События AFTER_CREATE_COMMITTED, AFTER_EDIT_COMMITTED и AFTER_DELETE_COMMITTED срабатывают только для backend'ов, откладывающих фиксацию транзакции до конца запроса; сегодня так работает только SQLAlchemy backend. О методах-хуках after_create_committed, after_edit_committed и after_delete_committed, которые их генерируют, см. раздел Представления.
Для события AFTER_DELETE_COMMITTED объект ctx.obj является отсоединённым (detached) экземпляром: уже загруженные атрибуты остаются доступны для чтения, но обращение к атрибуту, который не был загружен до удаления, вызовет исключение, поскольку соответствующая строка в базе больше не существует.
Каждый контекст — это dataclass, наследующийся от EventContext, который содержит поля, общие для всех событий:
| Атрибут | Тип | Описание |
|---|---|---|
event |
AdminEvent или str |
Сработавшее событие |
request |
Request |
Обрабатываемый запрос |
view_key |
str |
Значение key представления |
extra |
dict |
По умолчанию пустой; свободен для хранения данных в собственной цепочке обработчиков |
Каждый подкласс добавляет поля, относящиеся к соответствующему событию.
События редактирования, вызванные инлайн-редактированием со страницы списка, содержат extra["inline"] = True, а их полезные нагрузки data / old_data включают только изменённое поле. Всё остальное идентично обычному редактированию, поэтому существующие обработчики не требуют изменений.
Подписка с помощью декоратора
view.events.on() можно использовать и как декоратор, и как прямой вызов функции:
import logging
from starlette_admin.events import AdminEvent, BeforeDeleteContext
from starlette_admin.contrib.sqla import ModelView
logger = logging.getLogger(__name__)
class OrderView(ModelView):
fields = ["id", "customer_name", "total", "status"]
order_view = OrderView(Order, icon="fa fa-shopping-cart")
@order_view.events.on(AdminEvent.BEFORE_DELETE)
async def log_deletion(ctx: BeforeDeleteContext) -> None:
logger.info("Deleting order pk=%s", ctx.pk)
При такой регистрации log_deletion срабатывает только для order_view, но не для других представлений админ-панели. Метод on() также принимает обработчик напрямую, без формы декоратора:
AdminEventSubscriber: группировка обработчиков
Когда одна задача реагирует на несколько событий, AdminEventSubscriber позволяет собрать их в одном классе вместо того, чтобы разбрасывать функции по уровню модуля. Украсьте методы декоратором @on(AdminEvent.X) — здесь имеется в виду функция on уровня модуля из starlette_admin.events, а не метод шины событий, — а затем один раз вызовите subscribe():
import logging
from starlette_admin.events import (
AdminEvent,
AdminEventSubscriber,
AfterCreateContext,
AfterDeleteContext,
AfterEditContext,
on,
)
logger = logging.getLogger(__name__)
class AuditSubscriber(AdminEventSubscriber):
"""Журналирует каждое создание, обновление или удаление в любом представлении."""
@on(AdminEvent.AFTER_CREATE)
async def record_create(self, ctx: AfterCreateContext) -> None:
logger.info("created %s pk=%s", ctx.view_key, ctx.pk)
@on(AdminEvent.AFTER_EDIT)
async def record_update(self, ctx: AfterEditContext) -> None:
logger.info("updated %s pk=%s", ctx.view_key, ctx.pk)
@on(AdminEvent.AFTER_DELETE)
async def record_delete(self, ctx: AfterDeleteContext) -> None:
logger.info("deleted %s pk=%s", ctx.view_key, ctx.pk)
admin.events.subscribe(AuditSubscriber())
subscribe() доступен как на view.events, так и на admin.events. Вызовите его на view.events, чтобы ограничить действие подписчика одним представлением.
Один метод может обрабатывать несколько событий: @on(AdminEvent.AFTER_CREATE, AdminEvent.AFTER_EDIT) регистрирует один и тот же метод для обоих событий.
admin.events: делегирование представлениям
admin.events.on() принимает те же аргументы, что и view.events.on(), плюс keys= — список ключей представлений, ограничивающий область действия подписки. Если его не задать (None, значение по умолчанию), обработчик получит каждое текущее и будущее model view:
import httpx
from starlette_admin.events import AdminEvent, AfterCreateContext
@admin.events.on(AdminEvent.AFTER_CREATE, keys=["order"])
async def notify_new_order(ctx: AfterCreateContext) -> None:
async with httpx.AsyncClient() as client:
await client.post(SLACK_WEBHOOK_URL, json={"text": f"New order: {ctx.pk}"})
Этот обработчик вызывает только представление, зарегистрированное с key="order", либо представление, ключ которого по умолчанию разрешается в "order". Событие AFTER_CREATE любого другого представления не приведёт к его вызову.
admin.events.subscribe() тоже принимает keys=, поэтому AdminEventSubscriber можно ограничить подмножеством представлений тем же способом:
Параметр keys= влияет только на события жизненного цикла представлений из таблицы выше: создание, редактирование, удаление, action, экспорт и импорт. Именно так admin.events определяет, к каким представлениям применяется обработчик. Событие AFTER_LOGIN является событием уровня admin и не привязано ни к одному представлению, поэтому keys= для него ничего не делает.
Приоритет
on() принимает именованный аргумент priority — целое число, по умолчанию равное 0. Обработчики одного события выполняются в порядке убывания приоритета, то есть большее число срабатывает раньше:
import logging
from starlette_admin.events import AdminEvent, BeforeDeleteContext
logger = logging.getLogger(__name__)
@order_view.events.on(AdminEvent.BEFORE_DELETE, priority=10)
async def validate_can_delete(ctx: BeforeDeleteContext) -> None:
if ctx.obj.status == "shipped":
raise ValueError("Cannot delete a shipped order") # выполняется первым
@order_view.events.on(AdminEvent.BEFORE_DELETE, priority=0)
async def log_deletion(ctx: BeforeDeleteContext) -> None:
logger.info("deleting order pk=%s", ctx.pk) # выполняется вторым
Обработчики с одинаковым приоритетом выполняются в порядке регистрации. Методы AdminEventSubscriber принимают приоритет через @on(AdminEvent.X, priority=10), который передаётся таким же образом.
Warning
Обработчик BEFORE_DELETE или любой другой обработчик BEFORE_*, выбросивший исключение, останавливает операцию, и последующие обработчики этого события не выполняются. Исключение в обработчике AFTER_* превращает уже зафиксированное изменение в неудавшийся запрос. Если сбой не должен проявляться как ошибка админ-панели, заключайте рискованную логику — сетевые вызовы, обращения к сторонним API — в собственный блок try/except внутри обработчика.
Расширенный пример
Пример examples/05-events объединяет все паттерны с этой страницы: переопределения хуков в PostView, AuditSubscriber, зарегистрированный на admin.events для всех представлений, прямую регистрацию обработчиков предупреждений при удалении, экспорте и импорте, обработчик с областью действия post_view.events и CommentModerationSubscriber с областью действия comment_view.events. Запустите его, чтобы увидеть взаимодействие приоритета и области действия в одном приложении.
Что дальше
- Представления: методы-хуки
before_*иafter_*, на которых строится эта страница. - Действия: пакетные и строчные действия, генерирующие
BEFORE_ACTION/AFTER_ACTION. - Инлайн-формы: вложенные записи, создаваемые вместе с родительской записью.