skip to content

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%

answer

  1. each site is an instance
  2. the name becomes the namespace
  3. autodiscovery feeds only one
  4. same users, same permissions

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.

solid answer

~40 s

An `AdminSite` is just an object holding a registry of models and URLs, so you can instantiate several: `volunteer_site = VolunteerAdminSite(name="volunteers")`, mounted with `path("volunteers/", volunteer_site.urls)`. Its `urls` returns the patterns with app namespace `"admin"` and instance namespace equal to the site's `name`, so templates and `reverse()` work per site when `current_app` is set. Autodiscovery registers each app's `admin.py` on the default site only, so register models on the second site explicitly, with `volunteer_site.register(Shift, ShiftAdmin)` or `@admin.register(Shift, site=volunteer_site)` — the same model can have a different `ModelAdmin` on each site. Use it when two audiences need genuinely different surfaces: fewer models, simpler forms, other branding. It is not a security boundary by itself: a volunteer who is staff could still open the main site, so gate each site with its own `has_permission()` override.

code

python · 35 lines
python
# charity/admin_sites.py
from django.contrib import admin

from rota.models import Shift


class VolunteerAdminSite(admin.AdminSite):
    site_header = "Volunteer rota"
    site_url = None

    def has_permission(self, request):
        return super().has_permission(request) and request.user.groups.filter(
            name="volunteers"
        ).exists()


volunteer_site = VolunteerAdminSite(name="volunteers")


@admin.register(Shift, site=volunteer_site)
class VolunteerShiftAdmin(admin.ModelAdmin):
    fields = ["date", "start", "end", "role"]
    list_display = ["date", "start", "role"]


# urls.py
from django.contrib import admin
from django.urls import path

from charity.admin_sites import volunteer_site

urlpatterns = [
    path("admin/", admin.site.urls),
    path("volunteers/", volunteer_site.urls),
]

go deeper

for a junior

Recall that AdminSite can be instantiated more than once and each instance is mounted at its own URL.

for a middle

Explain explicit registration, the site name as instance namespace, and how current_app keeps links on the right site.

for a senior

Gate each site with has_permission and scope querysets, because both share users and permissions; test both registries.

for a principal

Decide whether separate admin sites or separate purpose-built interfaces serve distinct audiences better over time.

## What a second admin site is `django.contrib.admin.AdminSite` is an ordinary class. The familiar `admin.site` is one instance of it, created lazily from the admin app config's `default_site`. Nothing stops you from creating more: - each instance has its own **registry** (which models it shows, with which `ModelAdmin`); - its own **URLs**, mounted wherever you put `site.urls` in the URLconf; - its own **branding** and site-wide templates; - its own `has_permission()` gate. The constructor takes one argument, **`name`**, which becomes the site's URL instance namespace. The default site's name is `"admin"`. ## When it is worth it - **Different audiences.** Charity staff manage donors, donations and campaigns; volunteers only log shifts and see their rota. A slim volunteer site with two models and simple forms is easier to use and to reason about than hiding thirty models with permissions. - **Different presentations of the same model.** `Shift` can be registered on both sites with different `ModelAdmin`s — full fields for staff, three fields for volunteers. - **Separating an operations console** with its own URL and stricter gate from a content-editing site. When the audiences differ only in which models they may touch, groups and permissions on one site are usually enough. ## Wiring it 1. Subclass `AdminSite` if you need different attributes or a stricter `has_permission()`; otherwise instantiate `AdminSite(name="volunteers")` directly. 2. Register models explicitly: `volunteer_site.register(Shift, VolunteerShiftAdmin)` or `@admin.register(Shift, site=volunteer_site)`. **Autodiscovery only feeds the default site** — it imports every `admin.py`, whose `admin.site.register()` calls land on `admin.site`. 3. Mount both in the root URLconf: `path("admin/", admin.site.urls)` and `path("volunteers/", volunteer_site.urls)`. 4. Give each site its own templates or template attributes if the look differs. ## URL reversing across sites `site.urls` returns a triple: the patterns, the application namespace `"admin"`, and the instance namespace `site.name`. | You write | Resolves to | |---|---| | `reverse("admin:index")` | the default instance (named `admin`) | | `reverse("admin:index", current_app="volunteers")` | the volunteer site's index | | `reverse("volunteers:index")` | the volunteer site's index, by instance namespace | Admin templates use `{% url 'admin:...' %}`, which honours `request.current_app`; the admin's own views set it, and custom views on a site must set it too, or their links jump to the default site. ## It is not a security boundary on its own Both sites authenticate the same users against the same permissions. By default each admits any active staff user, so a volunteer with `is_staff` can type `/admin/` and see every model their permissions allow there. To separate audiences: - override `has_permission()` on each site, e.g. staff site requires the "staff" group, volunteer site requires "volunteers"; - keep permissions minimal, because they apply wherever a model is registered; - give each site's `ModelAdmin`s their own `get_queryset()` scoping where rows differ. ## Costs - Every registration is duplicated or split, so reviewers must check two registries. - Third-party packages that register on `admin.site` only appear on the default site. - Tests should hit both sites' URLs to catch a model missing from one. ## Testing both sites Two registries drift apart quietly, so a few tests pay for themselves: 1. For each site, log in as its intended user and GET its index; assert 200 and the expected header. 2. Log in as a volunteer and GET the staff site's index; assert the redirect to the login page, proving the `has_permission()` override works. 3. Reverse a model URL on each site with the right `current_app` and assert it resolves under that site's prefix. 4. Assert `volunteer_site.is_registered(Model)` is `False` for each sensitive model, so nobody registers one on the volunteer site by accident. ## Alternatives worth weighing - **One site, groups and hooks** — often enough when the difference is which models are visible. - **`ModelAdmin` methods keyed on the user** — `get_fields()`, `get_list_display()` and `get_queryset()` can vary by group on a single site. - **Purpose-built views** — when volunteers need a workflow rather than tables, a small set of ordinary views is simpler than any admin site.

  • Why does a model registered in an app's admin.py not appear on a second Django admin site?
    Autodiscovery imports each installed app's `admin.py`, and `admin.site.register()` or a bare `@admin.register` puts the model on the default site only. A second `AdminSite` starts with an empty registry, so every model it should show must be registered on it explicitly, with its own `ModelAdmin` if needed.
  • In a custom view on a second Django admin site, why do admin links point to the default site?
    Admin templates reverse `admin:` URL names, which resolve to the default instance unless `request.current_app` names another. The admin's built-in views set it; a custom view must set `request.current_app = self.admin_site.name` (or the site's name) before rendering.

saying these in an interview costs you the question

  • A second admin site has separate users and permissions
  • Autodiscovery registers every admin.py on all admin sites
  • A model can only be registered on one admin site
  • Staff users are automatically kept out of other admin sites