Массовые действия
Панель, которая появляется при отметке строк, — как объявить действие, в какой транзакции оно выполняется и как отказаться от них вовсе.
Доступно с версии v0.1.0
Отметьте строку — и над таблицей появится панель. delete встроено и включено
по умолчанию; всё остальное — методы на ModelAdmin.
@admin.register(Product, key="catalog.product")
class ProductAdmin(admin.ModelAdmin):
actions = ("delete", "activate", "mark_discontinued")
@admin.action("Activate", icon="check-circle")
async def activate(self, adapter, objects):
for product in objects:
await adapter.update(product, {"is_active": True})
return f"{len(objects)} products activated."
Декоратор несёт в себе подпись и иконку, поэтому они не могут разойтись с кодом, который их реализует, — а именно это допускало бы простое перечисление имён методов в списке.
@admin.action(label, *, icon=None, danger=False, confirm=None)
label | Что написано на кнопке |
icon | Имя из fastfort.ui.icons, проверяется на этапе объявления |
danger | Отрисовывается цветом опасности и спрашивает подтверждение перед запуском |
confirm | Текст вопроса. {count} подставляется автоматически |
@admin.action(
"Mark discontinued",
icon="trash",
danger=True,
confirm="Mark {count} products discontinued? They will stop appearing in the shop.",
)
async def mark_discontinued(self, adapter, objects):
for product in objects:
await adapter.update(
product, {"availability": Availability.DISCONTINUED, "is_active": False}
)
return f"{len(objects)} products discontinued."
Сигнатура
async def handler(self, adapter, objects) -> str: ...
adapter — это ModelAdapter для этой модели, привязанный к единице
работы текущего запроса. objects — уже загруженные строки, которые были
отмечены. Возвращаемое значение — сообщение, показанное после выполнения.
Действие никогда не коммитит
Это стоит усвоить как следует. Адаптер выполняет flush; UnitOfWork запроса
коммитит при штатном завершении и откатывается при исключении. Поэтому
действие, упавшее на сороковой строке, оставляет незаписанными и первые
тридцать девять — а это почти всегда то, что нужно от массовой операции, и не
то, что даёт цикл из отдельных сохранений.
@admin.action("Recalculate totals")
async def recalculate(self, adapter, objects):
for invoice in objects:
# If this raises on invoice 40, invoices 1–39 are rolled back with it.
await adapter.update(invoice, {"total": compute_total(invoice)})
return f"{len(objects)} invoices recalculated."
Не вызывайте commit() самостоятельно. Это разделит запрос на две транзакции
и нарушит эту гарантию для всего, что идёт следом.
Ни одного действия
actions = ()
Панель никогда не появляется. Так модель, строки которой нельзя удалять массово, заявляет об этом явно — кнопка удаления отдельной строки при этом не затрагивается.
@admin.register(ApiToken, key="accounts.token")
class ApiTokenAdmin(admin.ModelAdmin):
actions = ()
Доступность
Метод, помеченный @admin.action, но не включённый в actions,
недоступен — отправка его имени не сработает. Проверка идёт по
объявленному списку, а не по getattr — поэтому закомментированное в
actions имя отключает действие, а не просто скрывает кнопку.
Встроенное удаление
"delete" выполняет тот же план удаления, что показан на странице
подтверждения, и проверяет его повторно непосредственно перед записью —
потому что страница могла быть открыта час назад, а граф связей за это время
мог измениться.
Если хотя бы одна из выбранных строк защищена, не удаляется ничего, а страница сообщает, какая именно.
Ошибки
Бросьте исключение — и всё откатится, а оператору будет показано сообщение:
from fastfort import ValidationError
@admin.action("Ship", icon="truck")
async def ship(self, adapter, objects):
unpaid = [s for s in objects if s.customer and not s.customer.is_active]
if unpaid:
raise ValidationError(
f"{len(unpaid)} shipments belong to inactive customers.",
hint="Reactivate the customer, or deselect those rows.",
)
...
ValidationError отображается на странице как сообщение. Всё остальное в
продакшене превращается в общую ошибку — с трассировкой в логах, а не на
чьём-то экране.