skip to content

Admin Site

Django's admin builds staff CRUD screens from ModelAdmin classes: change lists, change forms, bulk actions and permission hooks. Interviewers ask what it is safe to expose and to whom.

part ofDjangooverview, primer and where to startread it →
on this pageshow

explore

questions

27

How do you register a model with Django's admin, and how do you choose the columns its change list shows?

level: juniorimportance: must knowfreq 62%

answer

  1. a module the admin discovers
  2. a function call or a class decorator
  3. a ModelAdmin subclass
  4. a tuple of column names

basics

~10 s

Register a model in the app's admin.py with admin.site.register(Ticket, TicketAdmin) or the @admin.register(Ticket) decorator on a ModelAdmin subclass. The change list shows the columns named in ModelAdmin.list_display, defaulting to the object's str.

solid answer

~40 s

The admin app autodiscovers each installed app's `admin.py`. In it you either call `admin.site.register(Ticket, TicketAdmin)` or decorate the `ModelAdmin` subclass with `@admin.register(Ticket)`; both register on the default site, and the decorator accepts `site=` for another one. Registering without a `ModelAdmin` gives the default options. The **change list** is the admin's per-model record page, and `list_display` chooses its columns: model field names, `__` paths to related fields (Django 5.1+), callables, `ModelAdmin` methods or model methods and properties. With no `list_display` you get a single column showing `__str__()`. A `ForeignKey` column shows the related object's `__str__()`, a boolean field shows yes/no icons, and `ManyToManyField` names are rejected because each row would need its own query.

code

python · 11 lines
python
from django.contrib import admin

from helpdesk.models import Customer, Ticket


@admin.register(Ticket)
class TicketAdmin(admin.ModelAdmin):
    list_display = ["subject", "status", "priority", "customer", "opened_at"]


admin.site.register(Customer)  # default ModelAdmin options

go deeper

for a junior

Know both registration forms, that they live in admin.py, and that list_display names the change list columns with str as the default.

for a middle

Explain autodiscovery, the five kinds of list_display entries, and the rendering rules for foreign keys, booleans and many-to-many fields.

for a senior

Show that you treat every list_display column as a query cost and keep registration consistent across several admin sites.

for a principal

Be ready to argue which models deserve an admin at all, since each registration is an internal UI you must secure and maintain.

## Where registration happens `django.contrib.admin` ships with an app config whose `ready()` calls `admin.autodiscover()`. That imports the `admin` module of every installed app, so the convention is to register models in `<app>/admin.py`. A model that is never registered simply does not appear in the admin. There are two equivalent ways to register: 1. **`admin.site.register(Ticket, TicketAdmin)`** — a function call on the default `AdminSite` instance. The `ModelAdmin` class is optional; `admin.site.register(Ticket)` uses the default options. It also accepts an iterable of models. 2. **`@admin.register(Ticket)`** — a class decorator on the `ModelAdmin` subclass. It takes one or more models and an optional `site=` keyword for a custom `AdminSite`. It raises `ValueError` if no model is passed or if the decorated class does not subclass `ModelAdmin`. Both paths end in `AdminSite.register()`, which raises `AlreadyRegistered` if the model is registered twice on the same site and `ImproperlyConfigured` for an abstract model. ## The change list and list_display The **change list** is the page at `/admin/<app>/<model>/`: a paginated table of records with search, filters and actions around it. `list_display` decides its columns. Without it, the page shows one column, the object's `__str__()`, which is why a readable `__str__` matters. `list_display` accepts five kinds of entries: | Entry | Example | Notes | |---|---|---| | Model field name | `"subject"` | sortable by clicking the header | | Related field with `__` | `"customer__email"` | added in Django 5.1 | | Callable taking the instance | `ticket_age` | defined at module level | | `ModelAdmin` method name | `"sla_state"` | `def sla_state(self, obj)` | | Model method or property name | `"is_overdue"` | no required arguments | ## Built-in rendering rules - A **`ForeignKey`** column shows the related object's `__str__()`. - A **`BooleanField`** column shows yes/no/unknown icons instead of `True`/`False`/`None`. - **`ManyToManyField`** names are not accepted: rendering them would run a separate query per row. Use a method that builds the text, knowing it costs queries. - Empty values (`None`, empty string, empty iterable) render as `-`, which `empty_value_display` changes. - Output from methods and callables is **HTML-escaped**; build markup with `format_html()`. - The first column links to the change form unless `list_display_links` says otherwise. ## A support-ticket example For a help-desk app, a first useful change list shows the subject, status, priority, the customer and when the ticket was opened. Everything else (filters, search, computed columns) builds on this registration. ## Other options that shape the list A handful of neighbouring `ModelAdmin` attributes control how that table behaves: | Attribute | Default | Effect | |---|---|---| | `list_display_links` | `()` | which columns link to the change form; empty means the first column, `None` means no links | | `list_per_page` | `100` | rows per page | | `list_max_show_all` | `200` | the "Show all" link appears only when the total is at or below this | | `ordering` | `None` | default sort; falls back to the model's `Meta.ordering` | | `sortable_by` | `None` | which columns get clickable headers; `None` means all sortable ones | | `empty_value_display` | the site's `-` | placeholder for empty values | When the resulting order is not guaranteed to be total, the change list appends `-pk` so pagination never shows a row twice or skips one. For a ticket queue, `ordering = ["-opened_at"]` and a `list_per_page` of 50 are typical first adjustments. ## Mistakes to avoid - Registering in `models.py`: it works only if that module is imported early, and it mixes admin concerns into the model layer. Put it in `admin.py`. - Registering the same model twice, often once with the decorator and once with a stray `admin.site.register()` call, raises `AlreadyRegistered` at startup. - Listing a reverse relation or many-to-many field directly in `list_display` and expecting it to render. - Forgetting that every extra column may add work per row; related columns and methods can multiply queries, which is a separate tuning question.

  • What does a Django admin change list show if list_display is not set?
    A single column containing each object's `__str__()`, linked to its change form. That is the default value of `list_display`, `("__str__",)`. It is why models that end up in the admin should define a meaningful `__str__`; otherwise staff see rows labelled like `Ticket object (42)`.
  • Why does Django's admin reject a ManyToManyField name in list_display?
    Showing a many-to-many value would need a separate query for every row on the page. The system checks refuse the field name. If the column is really needed, write a method that joins the related names and pair it with `prefetch_related` in `get_queryset()`, so the page still runs a fixed number of queries.

saying these in an interview costs you the question

  • Models appear in the admin automatically once they are migrated
  • admin.site.register() requires a ModelAdmin class as its second argument
  • list_display can show a ManyToManyField directly as a comma-separated column
  • A ForeignKey column in list_display shows the related row's numeric id
  • Registering a model twice on the same site just overrides the first registration
open as a page

In Django, what do the is_staff and is_superuser user flags each control, and can a superuser without is_staff use the admin?

level: juniorimportance: must knowfreq 62%

basics

~20 s

is_staff lets an active user into the Django admin; is_superuser makes has_perm() return True for every permission. They are independent: a superuser with is_staff=False cannot log in to the admin, and a staff user with no permissions sees nothing to edit.

open as a page

How do you restrict a Django admin action so only staff holding a custom 'ship order' permission can see and run it?

level: middleimportance: must knowfreq 50%

basics

~10 s

Pass permissions=["ship"] to @admin.action and define has_ship_permission(self, request) on the ModelAdmin. The admin hides the action from users failing every listed check and refuses a forged POST for it; per-object checks remain your job.

open as a page

How do you add a custom Django admin action that marks the selected orders as shipped, and what does the action function receive?

level: middleimportance: must knowfreq 60%

basics

~10 s

Write a function taking (modeladmin, request, queryset), decorate it with @admin.action(description=...), and list it in the ModelAdmin's actions. The queryset holds the ticked rows; returning None sends the user back to the change list.

open as a page

In Django's admin, how do you edit a course's lessons on the course's own change form, and when do you choose TabularInline over StackedInline?

level: middleimportance: must knowfreq 55%

basics

~20 s

Define a TabularInline or StackedInline subclass with model = Lesson and list it in the course ModelAdmin's inlines. Tabular renders one compact row per lesson, stacked renders each lesson as a full form; the only difference is the template.

open as a page

How do you add a custom donations report page to a Django admin, under the Donation model's URLs and inside the admin layout?

level: middleimportance: must knowfreq 48%

basics

~10 s

Override ModelAdmin.get_urls() and prepend a path whose view is wrapped in self.admin_site.admin_view(); render a template extending admin/base_site.html with admin_site.each_context(request). admin_view only checks staff access, so check model permissions inside the view.

open as a page

A Django admin change list over a 20-million-row donations table takes seconds per page; which admin options and overrides make it fast?

level: seniorimportance: must knowfreq 42%

basics

~20 s

A Django admin change list runs two COUNT queries per page by default. Set show_full_result_count = False to drop the unfiltered one, supply a paginator whose count is estimated or capped, and keep search, sorting, date_hierarchy and filters on indexed, cheap paths.

open as a page

In the Django admin, regional managers must see and edit only their own region's stores; how do you scope it, and where does get_queryset() alone leak?

level: seniorimportance: must knowfreq 45%

basics

~20 s

Override ModelAdmin.get_queryset() to filter by the manager's region; the change list, change and delete views and actions inherit it. It does not scope foreign-key choices on other forms, raw-id inputs, related list filters or inlines, so limit those too.

open as a page

In the Django admin, what does the built-in 'Delete selected' action do, and how do you remove it from one model or the whole site?

level: juniorimportance: should knowfreq 42%

basics

~20 s

Django's admin registers delete_selected on every AdminSite: it lists the ticked rows plus everything that would cascade with them, asks for confirmation, then logs and deletes them. Remove it site-wide with admin.site.disable_action(), or per model by overriding get_actions().

open as a page

In a Django ModelAdmin, how do fields, fieldsets, readonly_fields and prepopulated_fields shape the change form?

level: juniorimportance: should knowfreq 48%

basics

~20 s

fields picks and orders the inputs; fieldsets groups them under headings with classes and descriptions; readonly_fields shows values as text and removes them from the form; prepopulated_fields fills a slug from other fields with JavaScript.

open as a page

How do you replace the Django admin's 'Django administration' header and page titles with a charity's own branding?

level: juniorimportance: should knowfreq 50%

basics

~10 s

Set site_header, site_title and index_title on the Django admin site, either on admin.site directly or on an AdminSite subclass made the default through an AdminConfig subclass's default_site. Logos and colours come from overriding admin/base_site.html.

open as a page

A Django admin course form hangs because its instructor select lists every user; how do autocomplete_fields and raw_id_fields fix it, and how do they differ?

level: middleimportance: should knowfreq 42%

basics

~20 s

The default ForeignKey widget is a select filled with every related row. autocomplete_fields swaps in a search-as-you-type box that queries the related ModelAdmin's search; raw_id_fields swaps in a plain id input with a lookup popup.

open as a page

In Django's admin, how do you vary a change form per request, such as making price read-only for non-superusers, with get_form() and related hooks?

level: middleimportance: should knowfreq 36%

basics

~20 s

Override the get_* hook matching the option: get_readonly_fields(), get_fields() or get_fieldsets() receive request and obj. get_form() returns the ModelForm class, so override it to swap forms. Return new lists; never append to the class attribute.

open as a page

In Django's admin, how do you add a computed column to list_display, and what do @admin.display's arguments control?

level: middleimportance: should knowfreq 45%

basics

~20 s

Add a ModelAdmin method (or model method, property or callable) taking the object to list_display. @admin.display sets its header (description), yes/no icons (boolean), the field or expression to sort by (ordering) and the placeholder for empty values (empty_value).

open as a page

When would you run two Django admin sites, such as one for charity staff and one for volunteers, and how do you wire them?

level: middleimportance: should knowfreq 28%

basics

~20 s

Create a second AdminSite instance with its own name, register the models it should show on it explicitly, and mount its urls at another path. The name is its URL instance namespace; permissions still come from the same users and groups.

open as a page

How do you override a Django admin template for one model only, and which admin templates can only be overridden project-wide?

level: middleimportance: should knowfreq 40%

basics

~10 s

Put the template at templates/admin/<app_label>/<model_name>/<name>.html, extend the admin original and override one block. Only a listed set, such as change_form.html and change_list.html, is looked up per model; base_site.html and others are project-wide only.

open as a page

Why does Django's documentation call the admin an internal management tool, and what goes wrong when it becomes the customer-facing UI?

level: middleimportance: should knowfreq 45%

basics

~20 s

Django's admin is a model-centric interface for trusted staff: it exposes tables, relations, bulk actions and cascading deletes, with per-model permissions. Customers need process-centric flows, per-row rules and a hardened public surface, so they get ordinary views.

open as a page

Which ModelAdmin permission hooks does the Django admin call, and how would you make one model read-only for every staff user?

level: middleimportance: should knowfreq 48%

basics

~20 s

The Django admin calls has_view_permission, has_add_permission, has_change_permission and has_delete_permission on the ModelAdmin, plus has_module_permission for the app on the index. Returning False from add, change and delete leaves a read-only model for users with view rights.

open as a page

How do you make a custom Django admin action show an intermediate confirmation page before it changes any rows, as delete_selected does?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Return a TemplateResponse from the action on the first POST; its form re-posts the selected keys as _selected_action inputs, the action name and a marker such as apply. On that second POST the action sees the marker and does the work.

open as a page

Staff bulk-deleted orders via the Django admin's 'Delete selected' action and your Order.delete() cleanup never ran; how do you fix it in the admin?

level: seniorimportance: should knowfreq 38%

basics

~10 s

delete_selected calls ModelAdmin.delete_queryset(), whose default runs queryset.delete() and never calls Order.delete(). Override delete_queryset() to delete per object in a transaction, and keep delete_model() consistent for the single-object delete view.

open as a page

A Django admin change list for support tickets runs one extra query per row after adding a customer company column; why, and how do you fix it?

level: seniorimportance: should knowfreq 48%

basics

~20 s

By default the change list joins only ForeignKeys named directly in list_display; a method or customer__company__name path reaches the relation lazily per row. Set list_select_related to the relations used, or join them in get_queryset(), to return to a fixed query count.

open as a page

How would you reduce the attack surface of a production Django admin, including its URL and login page, and what does Django not do for you?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Mount the Django admin at a non-default path, keep staff and superuser accounts few, add a second factor, and restrict who can reach it. Django does not throttle admin logins; add rate limiting or override AdminSite.has_permission for extra conditions.

open as a page

In Django 6.1, how do you offer an admin action on a single order's change form, and what do location and description_plural change?

level: middleimportance: nice to knowfreq 18%

basics

~20 s

Django 6.1 added location= to @admin.action: ActionLocation.CHANGE_FORM puts the action on the change form, a list puts it on both. description labels it there; description_plural labels it on the change list and defaults to description.

open as a page

In Django's admin, what do show_facets and date_hierarchy add to a change list, and what extra queries does each one cost?

level: middleimportance: nice to knowfreq 28%

basics

~20 s

show_facets (Django 5.0+) shows per-choice row counts in the filter sidebar, at one aggregate query per filter; the default ALLOW computes them only when _facets is in the URL. date_hierarchy adds a date drill-down: a min/max query plus a distinct-dates query.

open as a page

What does django.contrib.admindocs add to the Django admin, and what must you configure to enable it?

level: middleimportance: nice to knowfreq 15%

basics

~20 s

django.contrib.admindocs builds staff-only documentation pages from the docstrings of models, views, template tags and filters. Enable it by adding the app, including its URLs before the admin's, and installing docutils; the admin then shows a Documentation link.

open as a page