Admin#
Register any Entity and get a paginated list/add/edit/delete UI, with
validation, permissions, and change history, for a few lines of
registration code.
The built-in admin#
This alone gives you /admin and /admin/users — a built-in user
management screen, gated and audited.
Registering your own models#
from hayavega.admin import admin_routes, ModelAdmin
from .models import Product
urlpatterns = [
*admin_routes(
DATABASE,
models=[
ModelAdmin(Product, page_size=50, can_access=lambda user: user.is_staff),
],
),
]
ModelAdmin fields:
| Field | Default | Meaning |
|---|---|---|
entity |
required | The Entity subclass to register |
name |
the entity's class name, lowercased | URL slug |
list_fields |
every declared column | Which columns the list view shows |
page_size |
25 |
Rows per page |
can_access |
user.is_superuser |
predicate(user) -> bool gate, same mechanism as superuser_required |
list_filter |
() |
Column names to add an exact-match dropdown for |
search_fields |
() |
Column names a search box ORs a substring match across |
actions |
{} |
Named bulk operations: {"name": callable(list[Entity])} |
list_filter/search_fields entries are validated against the
entity's declared columns at ModelAdmin(...) construction time --
typo a column name and it fails immediately, not on the first request
that hits it.
Filtering, searching, and bulk actions#
from hayavega.admin import ModelAdmin
from .models import Post
def archive(records):
for record in records:
Post.collection(DATABASE).seek(id__exact=record.id).revise(archived=True)
ModelAdmin(
Post,
list_filter=("published",),
search_fields=("title", "body"),
actions={"archive": archive},
)
list_filterrenders one<select>per named column on the list view, its options populated from the distinct values actually present in the table -- not a fixed, hardcoded choice list. Combines withsearch_fieldsvia AND.search_fieldsadds a search box (?q=...) that ORs a substring match (Selection.search) across the named columns. This is intentionally narrow -- not a generalQ()/OR-expression system -- scoped to exactly this one admin feature.actionsadds a checkbox per row and an action dropdown. Each action is a plaincallable(list[Entity])-- no special base class -- run against every record whose checkbox was selected when the list view's own form is submitted. An unregistered action name, or no rows selected, is a no-op (redirects back to the list unchanged).
What you get for free#
- Pagination —
?page=N, sized bypage_size; an out-of-range page clamps to the last page and a non-numeric page falls back to page 1, rather than erroring. - Validation — a blank required field or a type-coercion failure is a field-level form error (400, form re-rendered with the message), never a raw database constraint violation reaching the browser.
- CSRF on every add/edit/delete submission — the same
csrf_protectmechanism used elsewhere, extended to accept acsrf_tokenform field for plain HTML<form>submissions. - Change history — the edit page lists every create/update/delete recorded against that record, sourced from the audit log; updates record which field names changed, never the values, matching the audit log's no-sensitive-values convention throughout the framework.
See it end-to-end#
tests/test_admin_models.py exercises the full stack against a real WSGI
request/response cycle, including a cookie-authenticated session with a
correct CSRF token succeeding and an incorrect one being rejected with
403 — the same test shape you'd write for your own registered model.