07 · The Django Admin¶
The admin is a complete create/read/update/delete interface generated from your models. For an internal team managing content, orders or users, it can replace weeks of back-office work. It's also the fastest way to put real data into a project you're learning. This lesson takes it from the default to a properly configured screen, and is honest about where it stops being the right tool.
Turning it on¶
The admin is already in INSTALLED_APPS and config/urls.py (path("admin/",
admin.site.urls)) in a new project. You need a user who can log in:
It asks for a username, email and password, and runs the same password validators as
the rest of the site (too short or too common and it asks again). Visit /admin/. If
you're not logged in, you're redirected; we saw:
Nothing from catalog appears yet, because models have to be registered.
Registering models¶
The minimum is one line per model:
That gives a list of tags (shown using __str__, which is why every model should define
it) and an add/edit form built from the model's fields. blank, choices, max_length
and help_text all flow through into the form.
Configuring a ModelAdmin¶
For any model people will actually manage, write a ModelAdmin:
from django.contrib import admin
from .models import Author, Book, Review, Tag
class ReviewInline(admin.TabularInline):
model = Review
extra = 0
@admin.register(Book)
class BookAdmin(admin.ModelAdmin):
list_display = ["title", "author", "status", "pages", "published"]
list_filter = ["status", "tags"]
search_fields = ["title", "author__name"]
list_select_related = ["author"]
autocomplete_fields = ["author"]
filter_horizontal = ["tags"]
inlines = [ReviewInline]
actions = ["mark_done"]
@admin.action(description="Mark selected books as finished")
def mark_done(self, request, queryset):
updated = queryset.update(status=Book.Status.DONE)
self.message_user(request, f"{updated} book(s) marked finished.")
@admin.register(Author)
class AuthorAdmin(admin.ModelAdmin):
search_fields = ["name"]
admin.site.register(Tag)
What each option does:
| Option | Effect |
|---|---|
list_display |
columns on the change list (fields, methods, or callables) |
list_filter |
the filter sidebar; works on choices, booleans, dates and relations |
search_fields |
the search box; author__name searches across the join |
list_select_related |
controls which relations are joined into the list query |
autocomplete_fields |
replaces a <select> of every author with an AJAX search box |
filter_horizontal |
a two-pane picker for many-to-many fields |
inlines |
edit related rows (reviews) on the parent's page |
actions |
bulk operations on selected rows |
Other options you'll reach for: readonly_fields, fieldsets (group fields into
sections), prepopulated_fields (fill a slug from a title), date_hierarchy,
ordering, list_per_page, and list_editable (edit cells in the list itself).
The admin checks your configuration¶
autocomplete_fields needs the related model's admin to have search_fields, because
that's what the AJAX endpoint searches. When we left search_fields off AuthorAdmin,
python manage.py check refused to start:
ERRORS:
<class 'catalog.admin.BookAdmin'>: (admin.E040) AuthorAdmin must define "search_fields",
because it's referenced by BookAdmin.autocomplete_fields.
Admin mistakes (a typo in list_display, a non-existent field in search_fields) are
caught at startup like this, not when someone clicks the page. Run check often.
Measuring the change list¶
The admin is not magic about performance, so we measured it. Loading
/admin/catalog/book/ as a superuser ran 6 queries: the session, the user, the tag
list for the filter sidebar, two COUNT(*) queries (the filtered count and the
full-table count the paginator shows), and one query for the rows with the author joined. We then
set list_select_related = False and got the same 6: when a plain foreign key appears
in list_display, the admin applies select_related() for it automatically.
list_select_related matters when you want to control which relations are joined.
The automatic join doesn't help with methods. We added a column to AuthorAdmin
that returned obj.books.count(), and the author list went to 8 queries for 3 authors:
one extra COUNT per row. On a page of 100 authors that's 100 extra queries. Override
get_queryset() to compute the value in the main query instead:
from django.db.models import Count
@admin.register(Author)
class AuthorAdmin(admin.ModelAdmin):
list_display = ["name", "born", "book_count"]
search_fields = ["name"]
def get_queryset(self, request):
return super().get_queryset(request).annotate(n_books=Count("books"))
@admin.display(ordering="n_books", description="Books")
def book_count(self, obj):
return obj.n_books
@admin.display(ordering=...) also makes the computed column sortable.
Who can see what¶
Only users with is_staff=True can log into the admin. Inside it, a non-superuser sees
only models they have permissions for: Django creates add, change, delete and
view permissions for every model automatically, and you grant them to users or groups
(Level 2 · 06). Superusers bypass permission checks entirely, so don't hand that flag
out casually.
Custom actions should check permissions too. @admin.action(permissions=["change"])
hides the action from users who can't change the model.
When not to use the admin¶
The admin is built for trusted staff doing data management. It's a poor fit for:
- Customer-facing pages (users, not staff). Build real views.
- Complex multi-step workflows. Every page is a form for one model plus inlines.
- Very large tables without tuning. The default change list counts the whole table for
pagination; for tens of millions of rows, set
show_full_result_count = Falseand consider a custom paginator.
Treat the admin URL as sensitive. Moving it off /admin/ reduces noise from automated
login attempts, but it's not security on its own: strong passwords, and ideally
two-factor authentication via a third-party package, are what actually protect it.
How It Actually Works¶
admin.site is an AdminSite instance holding a registry: a dict from model class to
ModelAdmin instance. @admin.register and admin.site.register() add entries. On
startup, django.contrib.admin's AppConfig.ready() calls autodiscover(), which
imports the admin module of every installed app. That import is what runs your
registrations, and it's why admin.py must exist with that exact name.
admin.site.urls generates URL patterns from the registry: for each model, a change
list, add, change, delete and history view, all methods on its ModelAdmin. The change
form is a ModelForm built dynamically with modelform_factory() from the model's
fields, filtered by fields/exclude/readonly_fields. The change list is a
ChangeList object that builds a QuerySet from get_queryset(), applies the search
terms (as icontains lookups OR'd across search_fields), the filters and ordering,
and then paginates it. Everything is overridable because it's all plain methods on
ModelAdmin.
Common mistakes¶
- No
__str__on models, so the admin shows "Book object (3)". - Methods in
list_displaythat query relations (obj.books.count()), causing one extra query per row on every list page. - A
<select>with 50,000 options for a foreign key. Useautocomplete_fieldsorraw_id_fields. - Giving
is_superuserto staff who only need to edit a few models. Use groups. - Building customer features in the admin because it's quick. It becomes the product.
Exercise¶
- Create a superuser, register
Author,BookandTag, and add five books through the admin. - Configure
BookAdminwithlist_display,list_filter,search_fieldsandautocomplete_fields. Removesearch_fieldsfromAuthorAdminand read the check error. - Add the
book_countcolumn toAuthorAdminand make it sortable. - Write an action "Mark as want to read" and use it on two books.
- Create a staff user (not superuser) with only the "view book" permission and log in as them. What can they see?