Pages and plugins¶
Not everything in an admin is a table. A sales report, a settings form or a button that clears a cache are pages of your own. They sit next to the model views, in the same layout, behind the same sign in.
The examples on this page use the example models.
A page of your own¶
from typing import Any
from sqlalchemy import func, select
from starlette.requests import Request
from adminsite import AdminPage
class SalesReport(AdminPage):
label = "Sales report"
group = "Reports"
template = "reports/sales.html"
async def get_context(self, request: Request) -> dict[str, Any]:
async with self.admin.database.session() as session:
total = await session.scalar(select(func.sum(Order.total)))
return {"total": total}
admin = Admin(
engine, views=[OrderView], pages=[SalesReport], template_dirs=["templates"]
)
The page is served at /admin/-/sales_report, named after the class, and shows in the sidebar
under its group and in the command palette. Its template sits in
one of your template_dirs and extends the admin's layout:
{% extends "adminsite/base.html" %}
{% block header %}<h1 class="text-base font-semibold">{{ page.label }}</h1>{% endblock %}
{% block content %}
<div class="p-6">
<div class="stat">
<div class="stat-title">Sold so far</div>
<div class="stat-value">{{ total }}</div>
</div>
</div>
{% endblock %}
The admin's styles are daisyUI components, so a page can use the same
cards, stats and tables as the rest of the admin. The template gets page, and urls, user and
csrf_input as every page does. AdminPage lists the page's settings.
Who may open it¶
class SalesReport(AdminPage):
async def allows(self, request: Request) -> bool:
return request.state.user.is_manager
A refused page is left out of the sidebar and answers with a 403.
A form on the page¶
A page that takes a form overrides post. The form's token is checked first, so a form from
another site never reaches it:
from starlette.responses import RedirectResponse, Response
from adminsite.messages import add_message
class Settings(AdminPage):
template = "settings.html"
async def post(self, request: Request, form: dict[str, Any]) -> Response:
await save_settings(form)
add_message(request, "Settings saved.")
return RedirectResponse(request.url, status_code=303)
Put {{ csrf_input | safe }} inside the <form method="post"> in the template. To answer with
something other than a template, such as a file or JSON, override get and return any Starlette
response.
Plugins¶
A plugin packages views, pages, routes, templates and static files, so several projects can share them. It gets the admin once and adds what it brings:
from pathlib import Path
from adminsite import Plugin
HERE = Path(__file__).parent
class Reports(Plugin):
name = "reports"
def setup(self, admin: Admin) -> None:
admin.add_template_dir(HERE / "templates")
admin.add_static("reports", HERE / "static")
admin.add_stylesheet("-/static/reports/reports.css")
admin.add_page(SalesReport)
admin.add_view(ReportScheduleView)
admin.add_route("/-/reports/export.csv", export_sales)
admin = Admin(engine, plugins=[Reports()])
add_viewandadd_pageadd views and pages.add_routeadds an endpoint under/-/. Withguarded=Falseit stays outside the sign in, for a health check or a webhook.add_template_diradds templates, and can replace one of the admin's.add_staticserves a folder of files, andadd_stylesheetandadd_scriptadd a file to every page.
Admin says what each one takes. The same methods work on the admin directly,
without a plugin. Add routes and static files before the admin answers its first request, since
its routes are fixed from then on.