Putting a model in the admin: the register decorator, the ModelAdmin class, its full attribute set, and where registration should live.
from sillo.admin import ModelAdmin
@admin.register(Post)class PostAdmin(ModelAdmin): list_display = ["id", "title", "status", "created_at"]Or without a decorator:
admin.register(Post, PostAdmin)Both forms exist because a decorator reads better next to a class and the direct call is what you need when the pair is built dynamically.
The minimum
Section titled “The minimum”@admin.register(Tag)class TagAdmin(ModelAdmin): passEvery default applies: rows shown by __str__, no search, no filters, 25 per
page, delete as the only bulk action.
Which is worth doing for reference tables, and worth spending five more lines on for anything you will actually work in.
The attributes
Section titled “The attributes”| Attribute | Default | Controls |
|---|---|---|
verbose_name | class name | The label in the sidebar |
list_display | ["__str__"] | Columns in the list |
list_display_links | [] | Which columns link to the detail page |
list_filter | [] | Fields offered as filters |
search_fields | [] | Fields the search box looks in |
ordering | [] | Default sort |
list_per_page | 25 | Rows per page |
actions | ["delete_selected"] | Bulk actions |
fields | None | Fields on the form, in order |
exclude | None | Fields kept off the form |
readonly_fields | [] | Shown but not editable |
save_on_top | False | A second save button above the form |
A realistic one:
@admin.register(Post)class PostAdmin(ModelAdmin): verbose_name = "Posts" list_display = ["id", "title", "author", "status", "published_at"] list_display_links = ["title"] list_filter = ["status", "author"] search_fields = ["title", "body"] ordering = ["-published_at"] list_per_page = 50 readonly_fields = ["created_at", "updated_at"] exclude = ["deleted_at"]Each is covered in Customising.
Overriding the getters
Section titled “Overriding the getters”Every attribute has a classmethod behind it, so anything that has to be computed can be:
@classmethoddef get_list_display(cls): ...@classmethoddef get_search_fields(cls): ...@classmethoddef get_list_filter(cls): ...@classmethoddef get_ordering(cls): ...@classmethoddef get_fields(cls, add=False): ...@classmethoddef get_readonly_fields(cls, add=False): ...@classmethoddef get_queryset(cls, queryset): ...get_fields and get_readonly_fields take add (True on the create form,
False on edit) which is how a field is settable once and read-only
afterwards:
@classmethoddef get_readonly_fields(cls, add=False): return [] if add else ["slug"]get_queryset
Section titled “get_queryset”The one you will reach for most. It filters what the admin can see at all:
@classmethoddef get_queryset(cls, queryset): return queryset.filter(deleted_at__isnull=True)@classmethoddef get_queryset(cls, queryset): return queryset.select_related("author").prefetch_related("tags")That second form is worth doing whenever list_display names a relation.
Without it the list issues one query per row to render the author column, the
classic N+1, and very visible at 50 rows a page.
Where registration lives
Section titled “Where registration lives”The routes are built on startup, so every registration has to have run by then. The reliable place is a module imported during application assembly:
from sillo.admin import ModelAdminfrom app.bootstrap import adminfrom database.models import Post, Tag, User
@admin.register(Post)class PostAdmin(ModelAdmin): ...def create_app(): app = SilloApp() ... admin = setup_admin(app, title="Acme Admin", user_model=User) import app.admin # noqa: F401 — registers the models return appThe starter does this for you. Registering after startup silently does nothing: the model is in the registry, and no routes exist for it.
What is registered for you
Section titled “What is registered for you”- The user model, always: the site cannot authenticate without one, and browsing who can sign in is something every admin needs.
AdminActivity, the log.
AdminRole is not, since it only applies if you use the admin’s own user
model. Register it yourself if you do.
To replace the auto-registered user admin, register your own. Yours is what the registry ends up holding:
@admin.register(User)class UserAdmin(ModelAdmin): list_display = ["id", "email", "username", "is_staff", "is_active"] search_fields = ["email", "username"] list_filter = ["is_staff", "is_active"] readonly_fields = ["password", "created_at"]