Custom actions¶
An action is a handler method decorated with AdminHandler.action (or the backend subclass).
The plugin renders a button for every decorated method, and the method becomes its own route
on the admin API.
from muffin_admin import PWAdminHandler
@admin.route
class UserResource(PWAdminHandler):
class Meta(PWAdminHandler.Meta):
model = User
@PWAdminHandler.action("/user/disable", label="Disable Users", view="bulk")
async def disable_users(self, request, resource=None):
"""Mark selected users as inactive."""
ids = request.query.getall("ids")
await User.update(is_active=False).where(User.email << ids)
return {"status": True, "ids": ids}
@PWAdminHandler.action("/user/{id}/admin", view="show")
async def mark_admin(self, request, resource=None):
"""Give the user the admin role."""
resource.role = "admin"
await resource.save()
Action paths are absolute inside the admin API, which is mounted at {prefix}/api. The
example above answers POST /admin/api/user/disable and POST /admin/api/user/{id}/admin.
Keep the resource name in the path; nothing prefixes it for you.
The method is called with request and a resource keyword argument. resource is the loaded
object for paths with {id} and None otherwise. Actions run through the regular handler
lifecycle, so authorize, prepare_collection and prepare_resource apply.
Arguments¶
| Argument | Description |
|---|---|
paths |
One or more URL paths for the action; the first matching path is used by the button |
methods |
HTTP methods allowed for the route, by default any method |
hosts |
A host name (or a list of names) to bind the route to |
view |
Where to show the button: list, show, edit or bulk (a string or a list) |
icon |
Material-UI icon name |
label |
Button label; the humanized method name by default |
schema |
Marshmallow schema; the admin renders a form and passes validated data as data |
help |
A link to an action help page |
confirm |
Ask for confirmation before the request, either True or a message |
file |
Serve the response as a file download (list and show views only) |
Any other keyword argument is passed to react-admin as an action property. It can override
generated ones, for example title:
@PWAdminHandler.action("/user/greet", view="list", title="Say hello")
view decides which screens render the button:
list: one button above the list, no selection needed.show: one button on the detail page; the path with{id}is used.edit: one button on the edit page.bulk: one button above the list acting on the selected rows; the selected ids are sent as theidsquery parameter.
Forms and validation¶
With a schema, the button opens a modal form built from the schema fields and sends the
validated payload to the method as data:
import marshmallow as ma
class GreetSchema(ma.Schema):
name = ma.fields.String(required=True)
@PWAdminHandler.action(
"/user/greet",
"/user/{id}/greet",
label="Greeter",
view=["list", "show"],
schema=GreetSchema,
)
async def greet(self, request, resource=None, data=None):
"""Greet a user."""
return {"status": True, "message": f"Hello {data.name}"}
Invalid or missing data answers 400 with the marshmallow messages in errors. The form
inputs follow the same field mapping used for create/edit; metadata, required and
load_default are honored, and ra_inputs overrides apply.
Confirmation and feedback¶
confirm=Trueasks "Do you confirm this action?" before sending.confirm="message.key"asks the translated message.- A dict with
messageis shown as a success notification. - A dict with
redirectTonavigates there instead of refreshing the list.
File downloads¶
file=True fetches the response with the normal Authorization header and saves it as a
download. Only list and show views support it; bulk actions ignore file.
from muffin import Response
@PWAdminHandler.action(
"/message/data.csv", label="Download CSV", icon="Download", view="list", file=True
)
async def csv(self, request, **_):
"""Download CSV file."""
return Response(
content="id,created,title\n" + "\n".join(f"{m.id},{m.created},{m.title}" for m in ...),
content_type="text/csv",
headers={"Content-Disposition": "attachment; filename=messages.csv"},
)
The download is an authenticated fetch: the frontend sends the usual Authorization
header (or the cookie in cookies mode), and the current list filters as the where query
parameter. Read the token from the header in check_auth; see
Authentication.
Restricting method and host¶
@PWAdminHandler.action(
"/user/greet", methods=("POST",), hosts="example.com", view="list"
)
async def greet(self, request, resource=None):
...