19.3 The site’s own pages: the index, the login and AdminSite.each_context
The pages of an AdminSite that belong to no model: the index and each application’s index, the login, the logout and the password change; AdminSite, which decides who gets in; and AdminSite, the context every page of the admin begins with.
These pages are methods of AdminSite, all but the login wrapped in AdminSite (The admin’s URLs: AdminSite and AdminSite), which lets in only staff; the indexes show each user what there is for them.
Who gets in: AdminSite.has_permission
AdminSite is true when the request’s user is active and staff: in its docstring’s words, permission to view at least one page of the site. It asks no model’s permission; the model admins do, page by page (Permissions in the admin: has_change_permission and the others). It is asked by admin_view on every request but the login’s, by the login, and by each_context, whose "has_permission" shows the user’s links in the header.
staff_member_required puts the same test on a view outside the site, sending a refused request to "admin:login"; admindocs uses it. It does not call has_permission, so overriding that method does not change it.
What every page is given: AdminSite.each_context
Every page of the admin, the model admins’ included, starts its context from AdminSite, all but the change list’s “Database error” page and the page that closes a popup:
| key | value | used for |
|---|---|---|
"site_title" |
value Admin, “Django site admin” |
used for each page’s title |
"site_header" |
value Admin, “Django administration” |
used for each page’s header |
"site_url" |
value Admin, "/"; while it is "/", the request’s "SCRIPT_NAME" if not empty |
used for the “View site” link, none for None |
"has_permission" |
value has_permission |
used for the user’s links |
"available_apps" |
value Admin |
used for the navigation sidebar |
"is_popup" |
value False; a model admin’s popup pages set their own |
used for no header or sidebar in a popup |
"is_ |
value Admin, True |
used for whether the sidebar is drawn |
"log_entries" |
value Admin: a queryset of the log, content types and users joined |
used for the index’s recent actions |
"available_apps" is built on every page, sidebar or not, asking every registered model admin for permissions; ModelBackend keeps them on the user object, so nora’s pages sent two queries for them and piet’s, a superuser’s, none (Permissions: Permission, groups and has_perm). "log_entries" is not evaluated until the index’s get_admin_log tag takes the user’s last ten rows (The log and the history: LogEntry and history_view). Nora’s index sent five queries: the session, the user, two for permissions and the log; the application index, the first four.
The application list: AdminSite.get_app_list
AdminSite walks the registry, or one application label’s models. It passes over a model whose admin’s BaseModelAdmin, by default the user’s has_module_perms for the label, is false, and one for which ModelAdmin holds no true permission. A model kept gets its plural verbose name, its permissions, the change list’s URL when the user may view or change, with "view_only" true when the user may only view, and the add page’s URL when the user may add, grouped under its application config’s verbose_name. get_app_list sorts applications and models by name; the documentation suggests overriding it to reorder.
# a run of the example: GET /admin/ as each user
nora Fleet: Ports (View), Ships (Change)
piet Authentication and Authorization: Groups (Add, Change), Users (Add, Change)
Fleet: Ports (Add, Change), Ships (Add, Change), Voyages (Add, Change)
tomas 302, Location: /admin/login/?next=/admin/
Nora holds no permission in auth, so its models fail has_module_permission; Voyage passes it and fails get_model_perms.
The index and the application index
AdminSite.index renders AdminSite or "admin/ with the list and AdminSite, “Site administration”. It calls get_app_list itself and again through each_context, so the list is built twice, once for a sidebar its template leaves out.
AdminSite does it for one label, as “Fleet administration”, from AdminSite, "admin/ or "admin/. An empty list raises Http404, so staff with no permission in an application get a 404 for it, as nora did at /admin/auth/.
The login: AdminSite.login
The login is the one page not wrapped in admin_view, being for those not yet let in. It carries never_cache itself, and login_not_required for LoginRequiredMiddleware (Requiring a login: login_required, permission_required and LoginRequiredMiddleware). It hands the request to LoginView, with AdminSite or AdminAuthenticationForm and AdminSite or "admin/ (The login and password views: LoginView, LogoutView and PasswordChangeView).
It first decides where a login ends: the request’s "next" if RedirectURLMixin finds it safe, else the index. A "GET" from a user who passes has_permission is redirected there at once. The same URL goes into the context as "next", which the form posts back in a hidden field; a "POST" with no "next" ends at LOGIN_REDIRECT_URL. The context also has "app_path", where the form posts, and "username", for telling a signed-in user shown the form that they are not authorized.
AdminAuthenticationForm asks for “a staff account” in its message for "invalid_login", and its confirm_login_allowed, after the parent’s, refuses a user who is not staff with that message and code (The authentication forms: AuthenticationForm and the password forms). So tomas, with the right password, was told what a wrong one is told:
# a run of the example
GET /admin/login/, no cookie 200, the hidden "next" holding /admin/
POST /admin/login/, tomas's right password 200, "Please enter the correct username and password for a staff account. Note that both fields may be case-sensitive."
POST /admin/login/, nora's, next=/admin/ 302, Location: /admin/
POST /admin/login/, nora's, no next 302, Location: /accounts/profile/
nora signed in, GET ?next=/admin/fleet/ship/ 302, Location: /admin/fleet/ship/
tomas signed in, GET /admin/login/ 200, "You are authenticated as tomas, but are not authorized to access this page. Would you like to login to a different account?"
Logging out and changing the password
AdminSite.logout is a LogoutView, which logs out only on a "POST". Its context sets "has_permission" false, since, its comment says, the user is not yet logged out when the context is made. Unless a safe "next" or LOGOUT_REDIRECT_URL sends the user elsewhere, it renders AdminSite or "registration/, a template the admin ships.
AdminSite is a PasswordChangeView with AdminSite or AdminPasswordChangeForm, a PasswordChangeForm with a CSS class for required fields, ending at AdminSite, a PasswordChangeDoneView; the admin ships both templates. The session that made the change stays signed in (The user of a request: login, logout and AuthenticationMiddleware).
The JavaScript catalogue and “View on site”
AdminSite is a JavaScriptCatalog of the admin package’s translations (The language views: set_language and JavaScriptCatalog).
A change form’s “View on site” link comes from BaseModelAdmin: none when BaseModelAdmin is false, a callable’s URL, or, for an object with get_absolute_url, the site’s "view_on_site" URL. AdminSite raises PermissionDenied when the content type’s model is registered and its model admin’s has_view_permission is false, before any object is looked for; else it calls the content types’ shortcut (Sites: Site, get_current_site and the shortcut view). Nora got 403 for a voyage that exists and one that does not; piet got 404 for both, from shortcut.