The first time my model appeared in Django admin, seeing “Article object (7)” felt like success with an asterisk. Registration had worked, but the interface knew almost nothing about how a human would find, review, or safely edit that record. A good ModelAdmin closes that gap.
Quick answer
from django.contrib import admin
from .models import Question
@admin.register(Question)
class QuestionAdmin(admin.ModelAdmin):
list_display = ["question_text", "pub_date"]
search_fields = ["question_text"]
list_filter = ["pub_date"]Registration and presentation stay together
@admin.register(Question)registers the model with the default admin site.The decorated class holds display, form, query, and permission behavior for that model.
list_displayreplaces the single default object-label column with useful columns.search_fieldsenables admin search andlist_filteradds a filter sidebar.Importing
polls.adminhappens through Django app discovery when the app and admin are installed.
A realistic model to administer
from django.db import models
class Question(models.Model):
question_text = models.CharField(max_length=200)
pub_date = models.DateTimeField("date published")
is_active = models.BooleanField(default=True)
def __str__(self) -> str:
return self.question_text
class Choice(models.Model):
question = models.ForeignKey(
Question, on_delete=models.CASCADE, related_name="choices"
)
choice_text = models.CharField(max_length=200)
votes = models.PositiveIntegerField(default=0)
def __str__(self) -> str:
return self.choice_textReadable labels matter beyond the admin
Django uses
str(object)in default admin choices, logs, the shell, and other debugging contexts.Return a concise human label rather than a large text body, secret, or expensive relationship query.
The type annotation documents that
__str__returns text.related_name="choices"gives the reverse relationship a meaningful name for inlines and queries.Changing Python presentation code needs no migration; changing model fields does.
Build a useful change list and edit form
from django.contrib import admin
from .models import Choice, Question
class ChoiceInline(admin.TabularInline):
model = Choice
extra = 0
fields = ["choice_text", "votes"]
@admin.register(Question)
class QuestionAdmin(admin.ModelAdmin):
list_display = ["question_text", "pub_date", "is_active"]
list_display_links = ["question_text"]
list_filter = ["is_active", "pub_date"]
search_fields = ["question_text"]
date_hierarchy = "pub_date"
ordering = ["-pub_date"]
list_per_page = 50
fields = ["question_text", "pub_date", "is_active"]
inlines = [ChoiceInline]Each option solves a different staff task
list_displaychooses columns;list_display_linkschooses which column opens the record.Date and boolean filters let staff narrow the list without inventing query parameters.
Text search is convenient but can be expensive on large tables because its database lookup depends on configured fields and terms.
date_hierarchyadds time-based navigation andorderingmakes recency predictable.Pagination limits each response; it does not remove the cost of an expensive count or search.
A
TabularInlineedits child choices with their parent and works best for small bounded relationships.fieldsis an allowlist for the form; sensitive, generated, or operational fields should not appear casually.
Show a calculated column safely
@admin.register(Question)
class QuestionAdmin(admin.ModelAdmin):
list_display = ["question_text", "pub_date", "choice_count"]
@admin.display(ordering="choice_total", description="Choices")
def choice_count(self, obj: Question) -> int:
return obj.choice_total
def get_queryset(self, request):
return super().get_queryset(request).annotate(
choice_total=models.Count("choices")
)Avoid one query per row
A method in
list_displaycan calculate presentation, and@admin.displaysupplies its label and ordering expression.Annotating once makes the count part of the list query instead of calling
choices.count()for every row.This snippet also needs
from django.db import modelsinadmin.py.For foreign-key labels, use
list_select_relatedor a tailoredget_queryset()where measurement shows repeated queries.Inspect database queries and plans with realistic data; admin convenience can hide costly list pages.
Run Django’s configuration checks
python manage.py check
python manage.py check --deploy --settings=project.settings.productionSystem check identified no issues (0 silenced).Checks catch configuration, not workflow mistakes
The general check detects invalid admin options such as unknown fields and incompatible list settings.
The deployment check evaluates important security settings against the named production configuration.
A clean result does not prove staff have the correct permissions or that queries scale.
Run tests and manually exercise create, edit, delete, filters, search, pagination, and inline validation.
Test registration and access
from django.contrib import admin
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.urls import reverse
from polls.models import Question
class QuestionAdminTests(TestCase):
def test_question_is_registered(self):
self.assertIn(Question, admin.site._registry)
def test_staff_can_open_question_list(self):
user = get_user_model().objects.create_superuser(
username="admin-test", password="test-only-password"
)
self.client.force_login(user)
response = self.client.get(reverse("admin:polls_question_changelist"))
self.assertEqual(response.status_code, 200)Test behavior, not just imports
The registry assertion confirms the model’s admin module was discovered.
The admin URL name follows
admin:app_label_model_name_changelist.force_loginisolates admin authorization and rendering from the login form.Use a clearly test-only password; the test database is created separately and destroyed afterward.
Add limited-staff tests proving allowed and denied operations, plus query-count tests for expensive custom columns.
Permissions and data visibility
Model add, change, delete, and view permissions govern the standard admin actions.
A superuser bypasses normal permission checks; use limited staff accounts for routine work.
Override
has_view_permission,has_change_permission, or related hooks only with tested, comprehensible rules.Object-level permissions are not automatically provided by Django’s core model permission system.
Restricting buttons is not enough: scope
get_queryset()so a user cannot retrieve another tenant’s rows.Protect foreign-key and autocomplete querysets too, or forms may disclose out-of-scope objects.
When a registered model does not appear
The app is absent from `INSTALLED_APPS`: Django will not discover its normal admin module.
The model is not registered: add the decorator or
admin.site.register; imported models do not register themselves.The user lacks view/change permission: test with the intended staff role rather than assuming a rendering bug.
The model uses another admin site: confirm the URL points to the same
AdminSiteinstance used for registration.An import error stops autodiscovery: read startup logs and run
manage.py check.The model was already registered: remove the duplicate registration or deliberately unregister before replacing it.
The table is missing: create and apply migrations; registration does not create database schema.
Admin design checklist
Give models concise, safe
__str__labels.Expose only fields staff need and make immutable values readonly.
Design list columns, links, filters, search, ordering, and pagination for the actual workflow.
Use inlines only when the relationship size and validation experience remain manageable.
Measure relationship columns and computed fields for N+1 queries.
Test least-privilege roles and tenant/object scoping.
Keep destructive actions explicit, auditable, and reversible where possible.
Treat the admin as production software: HTTPS, secure cookies, monitoring, upgrades, and backups still apply.
Official Django references
Django admin reference documents registration,
ModelAdmin, inlines, forms, lists, searches, permissions, and query hooks.Django tutorial: customize the admin develops the polls example with fieldsets, inlines, columns, filters, and search.
Django model methods explains model instance behavior including string representation.
Django authentication describes staff users, permissions, and sessions.
Comments and corrections