Операции и расширения
Field, ModelSchema, Query, Relation описывают данные независимо от HTML.
Контролы настраиваются через FieldOptions; для новых экранов есть
страницы и шаблоны, для бизнес-правил — сервисный адаптер.
Lifecycle записи
- Перепроверить identity и право на операцию таблицы.
- Проверить CSRF, allowlist, типы полей и выбор связанных записей.
- Вызвать
OperationHooks.before_writeсо снимком операции. - Выполнить атомарную запись адаптера/сервиса с rollback при ошибке.
- После commit вызвать
after_write, записать безопасные метаданные и перенаправить.
from quite_admin import OperationHooks, current_operation
from quite_admin.errors import ValidationError
async def validate(operation):
if operation.values.get("stock", 0) < 0:
raise ValidationError({"stock": "Остаток не может быть отрицательным."})
async def notify(operation, saved):
await notifications.admin_change(operation.collection, operation.action)
admin = Admin(
auth=auth_backend, secret_key=session_secret,
hooks=OperationHooks(before_write=validate, after_write=notify),
)
Хуки бывают sync/async; sync-хуки не должны выполнять блокирующий I/O. Они работают
для create, edit со связями, delete и пользовательских действий. Operation содержит
request, актуальную identity, таблицу, модель, action, типизированный ключ, значения,
ключи связей и предыдущую запись. Mappings скопированы и неизменяемы на верхнем уровне;
изменение вложенного снимка не меняет данные предстоящей записи. Для преобразования
значений используйте parser поля или сервис.
current_operation() доступен в сервисах и ORM-хуках, включая worker синхронного
адаптера. Вне записи он возвращает None, контекст сбрасывается и при ошибке.
Не сохраняйте request/operation за пределами времени жизни запроса.
Before-хук выполняется вне транзакции адаптера и может видеть устаревшую запись.
Критичные инварианты и права на строку проверяются внутри транзакции сервиса.
After-хук — уведомление: при ошибке пишется quite_admin.after_write_failed без значений
и текста исключения; уже сохранённая запись не превращается в ошибку ответа. Для
надёжного аудита сохраняйте audit row/outbox внутри транзакции сервиса. Встроенного
durable audit store нет.
Сервисы и схемы форм отдельных моделей
Сохраните обычный адаптер и подключите бизнес-создание в настройках модели:
async def create_link(operation, payload):
user = await users.get(operation.identity.subject)
return await links.create(payload, user=user)
admin.register(Link, options=ModelOptions(
create_fields=("long_url", "organization_id"),
create_schema=LinkCreate,
create_handler=create_link,
update_schema=LinkUpdate,
))
create_handler(operation, payload) заменяет создание только этой модели. Остальные
модели и операции сохраняют поведение адаптера. Без create_schema payload — обычный
словарь; со схемой — проверенный объект Pydantic. Обработчик возвращает словарь скалярных
значений или ORM-объект с загруженными скалярными атрибутами. В редиректы, историю и
after-хуки попадают только поля схемы адаптера. Возвращайте полную сохранённую запись
с ключом, сгенерированными значениями и значениями по умолчанию.
Установите quite-admin[pydantic] для необязательных схем Pydantic 2 create_schema /
update_schema. FastAPI уже включает Pydantic. Схемы проверяются после CSRF, прав и
разрешённых скалярных полей, до lifecycle-хуков и сохранения. Ошибки полей отображаются
в форме, ошибки модели — сверху. Исходные значения и контекст ошибок не логируются.
Поля формы по-прежнему определяются адаптером и настройками create_fields, edit_fields
и input_fields. Схемы не добавляют колонки БД и не открывают поля
неявно.
Без собственного обработчика нормализованные отправленные значения сохраняются
штатной атомарной операцией с проверкой конкурентных изменений. Неотправленные значения
по умолчанию из схемы не записываются. Для частичных и inline-правок используйте схему
с необязательными полями: она получает изменения, без объединения с предыдущей записью.
Собственный обработчик получает полный проверенный payload с умолчаниями схемы и сам
определяет правила сохранения.
Если редактирование выполняет сервис, задайте update_handler:
async def update_link(operation, payload, *, guard):
return await links.update(
operation.key, payload, actor=operation.identity.subject,
selections=operation.relations, guard=guard,
)
Сервис отвечает за единую транзакцию, откат и преобразование ошибок. Ненулевой guard
нужно проверить по свежим заблокированным данным внутри этой транзакции, как описано
в контракте checked-адаптера. guard=None означает, что адаптер
не поддерживает проверяемую запись. Проверять старый снимок перед отдельной записью
недостаточно. Связи из operation.relations также проверяются и сохраняются атомарно:
обработчик заменяет всю операцию, включая связи. Обработчики работают внутри
current_operation(), между обычными before/after-хуками. Синхронные обработчики не
должны выполнять блокирующий I/O. Ожидаемые ошибки сервиса преобразуйте в публичные
ValidationError, Conflict или NotFound.
Действия записи
from quite_admin import ModelOptions, RecordAction
async def publish(operation):
return await products.publish(operation.key, actor=operation.identity.subject)
admin.configure("products", options=ModelOptions(
actions=(RecordAction("publish", "Опубликовать", publish),),
))
Кнопки появляются на единой странице сущности. Выполнение требует POST, сессию,
CSRF и по умолчанию право edit. Для разрушительного действия задайте
required_permission="delete"; дополнительный callback задаётся через permission.
Handler принимает Operation, управляет своей транзакцией и возвращает scalar mapping
или None; после него выполняется переход к текущей записи. Действия, удаляющие
запись, требуют собственного подтверждения и перехода в приложении. GET действия
не выполняет. Handler может выбрасывать публичные ошибки адаптера. Подпись действия
задаётся на языке настроенного интерфейса.
Совместимость определяют ADAPTER_API_VERSION и контракт адаптера.
Границы шаблонов и расширений описаны в гарантиях публичного API.
Массовые действия
По умолчанию у каждой таблицы есть Удалить выбранные записи с учётом обычного
права delete и can_delete. Регистрировать его не нужно. Отключить только массовое
удаление можно через ModelOptions(bulk_delete=False); одиночное удаление остаётся доступным.
Когда на странице есть записи и разрешённая команда, рядом со строками появляются флажки,
а справа в панели таблицы, рядом с экспортом и созданием, — компактная иконка
Массовые действия, такого же размера, как иконка фильтров. Выберите отдельные строки
или всю текущую страницу флажком в заголовке и откройте меню иконки. Метка на ней показывает
число выбранных записей; список С выбранными позволяет выбрать команду.
Продолжить открывает отдельное подтверждение с подписями записей. Подтвердите выполнение
или отмените его, чтобы вернуться с прежними поиском, фильтрами и пагинацией. Escape закрывает
меню и возвращает фокус на иконку; нажатие снаружи также закрывает его. Выбор отдельных строк
и подтверждение работают без JavaScript. Можно выбрать до 100 разных записей; выбор
не распространяется на остальные страницы. Без разрешённых команд иконка и флажки скрыты.
Чтобы добавить команду только в список одной таблицы, передайте RecordAction в bulk_actions:
from quite_admin import ModelOptions, RecordAction
# Используйте Admin, модель Product и адаптер из настройки вашего приложения.
async def deactivate_product(operation):
return await adapter.update(operation.model, operation.key, {"active": False})
admin.register(Product, adapter=adapter, options=ModelOptions(
bulk_actions=(RecordAction("deactivate", "Деактивировать товары", deactivate_product),),
))
Команда появляется рядом со встроенным удалением только у Products. После подтверждения
каждый выбранный товар получает active=False; остальные товары и таблицы не меняются.
По умолчанию bulk_actions=(). Его элементы всегда доступны для массового выполнения:
bulk=True не требуется, кнопки в карточке и адреса одиночного выполнения не добавляются.
При автообнаружении используйте admin.configure("products", options=...).
Параметры заменяют всю конфигурацию таблицы: сохраните нужные настройки полей и прав.
Попробуйте действие в локальном FastAPI/Litestar-примере:
откройте Products, выберите два активных товара, команду Deactivate products,
продолжите и подтвердите. Оба значения Active станут ложными, результат покажет два успеха.
Обработчик находится в examples/common.py.
Автономное браузерное демо не выполняет Python-обработчики и массовые действия.
Чтобы использовать обработчик и в карточке записи, и в списке, по-прежнему задавайте
actions с bulk=True:
from quite_admin import ModelOptions, RecordAction
async def reset_counter(operation):
return await counter_service.reset(operation.key, user=operation.identity)
admin.configure("counters", options=ModelOptions(actions=(
RecordAction("reset", "Сбросить счётчики", reset_counter, bulk=True),
)))
Существующий API RecordAction(bulk=True) не изменился; у элементов actions по умолчанию
остаётся bulk=False. Идентификаторы должны быть уникальны среди actions и bulk_actions
одной таблицы; в других таблицах их можно повторять. Для общей кнопки зарегистрируйте
действие один раз в actions с bulk=True, не дублируя его идентификатор в обоих параметрах.
Оба способа по умолчанию требуют право edit. required_permission выбирает обычное право
таблицы, а permission добавляет синхронный или асинхронный callback. Для каждого ключа заново
проверяются права list и действия; в request.path_params["key"] передаётся его ключ.
Используются существующие Operation, current_operation(), before_write и
after_write. Правила, которым нужны данные записи, проверяйте в обработчике или
before_write. Новые методы адаптера и отдельный класс массового сервиса не нужны.
Подтверждение подписано, связано с таблицей, командой, пользователем и сессией
и действует 15 минут. Для обоих шагов нужен CSRF. Перед каждой командой проверяется
отпечаток записи на момент подтверждения. Это проверка перед вызовом, а не атомарный
compare-and-delete и не замена транзакции/блокировке обработчика. За транзакцию и
идемпотентность обработчик отвечает так же, как в одиночном действии. Команды
выполняются последовательно в запросе, максимум для 100 записей; фоновые задания
и выбор между страницами не создаются.
Результат показывает успех или ошибку каждой записи. Ошибка не останавливает остальные
записи; завершённые изменения сохраняются. Общей транзакции на весь набор нет.
Каждая операция получает собственное событие истории согласно history_level;
ошибки записываются на уровнях "audit" и "all". Неожиданная ошибка отображается
общим сообщением, а лог содержит только тип исключения и метаданные команды.