When would you run two Django admin sites, such as one for charity staff and one for volunteers, and how do you wire them?
answer
- each site is an instance
- the name becomes the namespace
- autodiscovery feeds only one
- same users, same permissions
basics
~20 sCreate 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 sAn `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# 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
Recall that AdminSite can be instantiated more than once and each instance is mounted at its own URL.
Explain explicit registration, the site name as instance namespace, and how current_app keeps links on the right site.
Gate each site with has_permission and scope querysets, because both share users and permissions; test both registries.
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