Registering models, who is allowed in, the activity log, and why the admin uses your user model rather than one of its own.
The Admin Panel
Section titled “The Admin Panel”At /admin/. Note the trailing slash, the routes need it.
sillo user:admin ada@example.com adauvicorn app:app --reloadThen sign in at http://localhost:8000/admin/ with that email and password.
One user model
Section titled “One user model”There is no separate administrator account. Sign-in is checked against
your project’s User, so people use their normal account, and adding a
field to User adds it everywhere.
That works because the admin’s own default user model and yours both extend
sillo.users.UserBaseModel, the same set_password, check_password and
verify_credentials. Passing yours replaces the other outright rather than
adapting to it:
admin = AdminSite( title="Myapp Admin", prefix=config.admin_prefix, user_model=User,)database/config.py therefore registers sillo.admin.models (the activity
log) but not sillo.admin.default_user:
MODEL_MODULES = ["database.models", "sillo.admin.models"]That second module holds AdminUser and AdminRole. Registering it would
create admin_users and admin_roles beside your users: a second set
of accounts to keep in step, or to forget about, that nothing would ever
write a row to.
Registering models
Section titled “Registering models”In app/admin.py, inside register_admin:
@admin.register(Post)class PostAdmin(ModelAdmin): verbose_name = "Posts" list_display = ["id", "title", "author_id", "published_at"] search_fields = ["title", "body"] list_filter = ["published_at"] readonly_fields = ["created_at"] ordering = ["-id"]| Attribute | |
|---|---|
verbose_name | What the sidebar calls it |
list_display | Columns on the list page |
search_fields | What the search box searches |
list_filter | Fields offered as filters |
readonly_fields | Shown but not editable |
ordering | Default sort, - for descending |
What the admin gives you per model
Section titled “What the admin gives you per model”For every registered model, mounted under the admin’s prefix:
| Route | |
|---|---|
/admin/<model>/ | List, with search, filters and pagination |
/admin/<model>/create/ | Create form |
/admin/<model>/<id>/ | Detail |
/admin/<model>/<id>/update/ | Edit form |
/admin/<model>/<id>/delete/ | Delete, with confirmation |
/admin/<model>/export/ | Export the current list |
/admin/<model>/bulk/ | Bulk actions |
<model> is the class name lowercased. Post becomes /admin/post/.
Password fields get a dedicated widget with a strength meter and a
confirmation field named password__confirm. Submitting the form without
the confirmation returns the form with “Passwords do not match” rather
than creating an account with an unverified password.
Who may enter
Section titled “Who may enter”An account needs is_staff. sillo user:admin sets it, along with
is_superuser.
The rule is active, and staff or superuser, and it is checked at sign-in and on every request:
@staticmethoddef may_enter(user) -> bool: if not getattr(user, "is_active", True): return False return bool(getattr(user, "is_staff", False) or getattr(user, "is_superuser", False))The account is read on each request rather than trusted from the session,
which carries only an identity and a display name. So clearing is_staff
or is_active takes effect on that person’s next request, not at
their next sign-in.
Promoting and revoking
Section titled “Promoting and revoking”from sillo.users.commands import find_user, set_staff
user = await find_user("ada@example.com", model=User)await set_staff(user, True, model=User) # let them inawait set_staff(user, False, model=User) # and out againOr edit the is_staff checkbox on the user’s own admin page, which is why
User is registered in the admin at all.
The activity log
Section titled “The activity log”sillo.admin.models provides AdminActivity: who did what, to which
model, and when. It is registered by default and appears in the sidebar as
Activity Log.
user_email action model_name object_idada@example.com login User —ada@example.com create Users 7ada@example.com export Users —Writes to it are best-effort (a failure to record must not fail the action being recorded) so a missing table means the log is simply empty rather than that the admin breaks.
Turning it off
Section titled “Turning it off”Remove it from MODEL_MODULES and migrate:
MODEL_MODULES = ["database.models"]sillo db:make drop activity log --applyThe sidebar entry disappears with the table. That is deliberate: the admin registers the log without knowing whether your application wanted it, so a registered model with no table is a real case, and a nav link that leads to a 500 is worse than no link.
Permissions
Section titled “Permissions”ModelAdmin exposes three hooks, called per request:
@admin.register(Post)class PostAdmin(ModelAdmin): def has_add_permission(self, request) -> bool: return request.user.is_superuser
def has_change_permission(self, request) -> bool: return True
def has_delete_permission(self, request) -> bool: return request.user.is_superuserThey control the buttons on the dashboard and the list page. Anything enforcing a rule that matters should also be enforced in the model or the route. An admin that hides a button has hidden a button.
The query console at /admin/query/ is superuser-only regardless: it
grants read and write on every table, so being signed in is not enough.
Customising the site
Section titled “Customising the site”admin = AdminSite( title="Myapp Admin", # header and browser tab prefix="/admin", # from config.admin_prefix user_model=User, auth_backend=None, # bring your own — see below)For authentication that is not sessions (SSO, LDAP, a proxy header) subclass
AuthBackend:
from sillo.admin.auth import AuthBackend
class HeaderAuth(AuthBackend): async def authenticate(self, request) -> bool: return request.headers.get("X-Forwarded-Email") in ALLOWED
async def get_user(self, request): return {"id": request.headers.get("X-Forwarded-Email"), "display_name": "SSO"}
admin = AdminSite(title="Myapp Admin", auth_backend=HeaderAuth())A backend with no user_model is left alone by the checks that assume
one.
Disabling the admin
Section titled “Disabling the admin”ADMIN_ENABLED=falseapp/bootstrap.py reads config.admin_enabled and skips registration
entirely. Nothing is mounted, no middleware is added, and /admin/ is a
404.
Useful for an API-only deployment of the same codebase.
Things that will bite you
Section titled “Things that will bite you”-
Trailing slashes.
/admin/login/, not/admin/login. -
The login field is named
email. It accepts an email or a username as the value, but a form postingusername=fails silently. -
Register models before
admin.mount(), or the default presentation wins. -
The session middleware must stay, even if the rest of the application moves to JWT. The admin authenticates through it.
-
Registering the admin after the middleware block makes every admin page 500 with “No Session Middleware Installed” while the session middleware is demonstrably installed. See Project Structure.
Related
Section titled “Related”- Users & Authentication: the model the admin signs in
- Database & Migrations: what
MODEL_MODULESdecides - Project Structure: where
app/admin.pysits - Middleware: why registration order matters