Admin Panel¶
A CRUD UI, generated from a model's own columns, for the models you register -- list, create, edit, and delete pages with no hand-written templates. This is a lightweight v1, not a Django-admin clone -- see "What this isn't" below before reaching for it on something it doesn't fit.
Setup¶
# main.py
from zeython import AdminServiceProvider
from app.Models.post import Post
from app.Models.user import User
app.register(AdminServiceProvider(
app,
models=[Post, User],
guard=lambda user: user.is_admin,
))
Visit /admin.
guard is a required argument, not optional -- there is deliberately
no default that lets any logged-in user in. Every admin route also
requires a logged-in user regardless of what guard says (see
Authentication); guard decides who, among logged-in
users, gets in. It can return (or await to) a bool:
guard=lambda user: user.is_admin # a boolean column
guard=lambda user: user.role == "staff" # a role column
guard=lambda user: Gate.role("admin")(user) # reusing an existing Gate check
A user who's logged in but fails guard gets a 403, not a redirect to a
login page they'd already pass -- they're already authenticated, they're
just not allowed here.
What gets shown¶
For each registered model:
- List (
/admin/{table_name}) -- every column not in__hidden__, paginated 20 per page. - Create (
/admin/{table_name}/new) -- a form field per column, excludingid,created_at,updated_at,is_deleted,deleted_at(managed byModelitself, never user-editable) and anything in__hidden__(a password hash has no business in a generic admin form). - Edit (
/admin/{table_name}/{id}/edit) -- the same fields, pre-filled with the row's current values. - Delete -- a button on the list view; soft-deletes via
instance.delete()(the same defaultsave()/delete()behavior documented in Database & Migrations).
A column's SQLAlchemy type picks its input: Boolean → checkbox,
DateTime → a datetime-local input, Integer/Float → a number
input, everything else → a plain text input.
Validation¶
A model's __rules__ (see Validation) run on every
create/edit through save(), same as anywhere else in the framework --
a failing rule redirects back to the form with the error shown instead of
saving. A raw database error (e.g. a NOT NULL violation on a column
with no explicit rule) is caught too and shown the same way, rather than
a raw 500.
How form submission actually works¶
CsrfMiddleware reads its token from a header
(X-CSRF-Token, see CSRF Protection), not a form field, and a
plain HTML <form action="..."> submission can't set a custom header. The
admin UI's generated forms carry a small inline script that intercepts
submit, reads the CSRF cookie, and re-issues the request via fetch
with the header attached -- the same pattern
Server-rendered forms documents for your
own forms, applied here automatically so you don't have to wire it up per
model. Create/edit/delete all go through this, so the browser's native
form submission (which can only ever be GET/POST, no PUT/DELETE)
never actually reaches the server directly.
What this isn't¶
- No relationship pickers. A foreign key column (
author_id) is a plain number input -- you type the related row's ID. There's no dropdown, search, or autocomplete over the related table. - No search or filtering. The list view is a straight paginated dump of every row.
- No bulk actions, no field-level permissions, no audit log. One
guardcovers every model and every action uniformly. - Not a public-facing UI. It's for internal, trusted-staff CRUD over
your own models -- unstyled-but-usable HTML, not something you'd ship
to end users. If you need any of the above, this is a reasonable
starting point to fork from (it's under 400 lines in
zeython/admin.py), not a plugin system to configure around.