Custom pages
Custom pages live on the AdminSite. They reuse the Chassis shell
(sidebar, navbar, brand, breadcrumbs) but are not ModelAdmins. Each class
must declare exactly one access contract.
| Class | Access | Groups |
|---|---|---|
PermissionAdminPage | PermissionOption with a view_* codename | may join navbar / dashboard groups |
PersonalAdminPage | authenticated staff | cannot join groups |
SuperuserAdminPage | active superuser | may join groups |
All of them subclass AdminPage. Site construction raises
ImproperlyConfigured if a class mixes two contracts, omits all three, or
puts a personal page in a group.
from django.http import HttpRequest, HttpResponse
from django.utils.translation import gettext_lazy as _
from django_chassis.options import AdminPageGroupOption, PermissionOption
from django_chassis.views import PermissionAdminPage
reports_group = AdminPageGroupOption(slug='reports', label=_('Reports'), icon_class='fa-solid fa-chart-line', order=10)
view_inventory = PermissionOption(
app_label='catalog', model='book', codename='view_inventory', name=_('Can view inventory')
)
class InventoryPage(PermissionAdminPage):
slug = 'inventory'
title = _('Inventory')
label = _('Inventory')
description = _('Stock overview')
icon_class = 'fa-solid fa-boxes-stacked'
permission = view_inventory
navbar_group = reports_group
dashboard_group = reports_group
def view(self, request: HttpRequest) -> HttpResponse:
return self.render(request, rows=[])
Register the class on the site:
class CustomAdminSite(ChassisAdminSiteMixin, AdminSite):
chassis_page_classes = (InventoryPage,)
AdminPageService mounts the routes. Put a PermissionAdminPage in
sidebar_items with SidebarPageItemOption(page_slug='inventory') if it
should appear in the sidebar. Personal and superuser pages cannot be
sidebar children.
Page anatomy
Default template: admin/chassis/page.html.
┌─ breadcrumbs ─────────────────────────────────────────────┐
│ Dashboard › Sidebar section › page label │
├─ toolbar (`page_actions`) ────────────────────────────────┤
│ optional page-level buttons (information pages) │
├─ title / description ─────────────────────────────────────┤
├─ `chassis_page_content` ──────────────────────────────────┤
│ whatever `view()` / `render()` puts in context │
└───────────────────────────────────────────────────────────┘
The shared page shell uses the full content width and renders form validation
errors through the common danger alert. Use chassis-page-actions for a
consistent right-aligned lower action panel and
admin/chassis/back_button.html for navigation back. The shared
form_rows.html template marks required fields and moves required confirmation
checkboxes after the other controls.
dispatch() checks has_view_permission. Failure raises
PermissionDenied (Chassis error page if
AdminErrorPageMiddleware is installed). Success calls view().
render(request, **context) merges:
admin_site.each_context(request)(brand, sidebar, navbar, footer)chassis_page— the page instancetitle/sidebar_section_labelfromAdminSidebarService- any extra kwargs you pass
AdminPage attributes
| Attribute | Type | Default | Purpose |
|---|---|---|---|
slug | str | required | URL segment and sidebar reference |
title | localized string | required | Heading |
label | localized string | required | Navigation label |
description | localized string | '' | Optional lead |
icon_class | Font Awesome class | 'fa-solid fa-table-list' | Sidebar / groups |
order | int | 0 | Sort key among pages |
template_name | str | 'admin/chassis/page.html' | Override per page |
filters_fieldset_title | localized string | _('Фильтры') | If the page renders filters |
chassis_settings | Settings | Settings() | Same collapsed / add flags as ModelAdmin |
navbar_group | AdminPageGroupOption or None | None | Navbar dropdown |
dashboard_group | AdminPageGroupOption or None | None | Dashboard actions section |
route_prefix | str | 'pages' | First URL segment |
URL: {admin}/{route_prefix}/{slug}/. URL name: chassis_page_{slug}
(namespaced with the AdminSite name).
Implement view(self, request) (not get() / post() on the base —
dispatch always calls view). Branch on request.method inside
view if you need POST.
Access contracts
PermissionAdminPage
permission must be a PermissionOption whose codename starts with
view_. The site registers it automatically. The check uses
AdminPermissionService / has_request_user_permission (group-only by
default).
PersonalAdminPage
Any authenticated staff user. Must not set navbar_group or
dashboard_group. Use this for "my profile", "my tokens", and similar
per-user screens.
SuperuserAdminPage
request.user.is_active and is_superuser.
Object pages
AdminObjectPage[T] extends a custom page with a list URL, an
object card, and @object_action methods. The object does not
have to be a Django model: you resolve it yourself.
Extra routes:
| Route | Name |
|---|---|
{prefix}/{slug}/ | chassis_page_{slug} — list (view) |
{prefix}/{slug}/{object_id}/ | chassis_page_{slug}_object |
{prefix}/{slug}/{object_id}/actions/{action_name}/ | chassis_page_{slug}_object_action |
You implement:
| Method | Role |
|---|---|
view(request) | list page |
get_object(request, object_id) -> T | None | None → HTTP 404 |
get_object_context_data(request, object_id, obj) | extra card context |
object_actions | list of @object_action method names |
Card template: admin/chassis/object_page.html (object_template_name).
The card toolbar lists allowed object actions. Permission and
condition / condition_method are re-checked on the action URL.
If the object disappeared, the action redirects to the list with an error message. Unknown or forbidden actions redirect back to the card.
Information pages
AdminInformationPage is a structured report (sections, alerts, header
actions, timelines). See Information pages.
See also
Wizard pages
Added in 1.0.22WizardAdminPage adds step navigation to the standard page shell. Combine it
with exactly one access contract (PermissionAdminPage, PersonalAdminPage or
SuperuserAdminPage). Register the resulting class in chassis_page_classes.
from django.http import HttpRequest, HttpResponse
from django_chassis.dto import WizardStepDTO
from django_chassis.views import SuperuserAdminPage, WizardAdminPage
class SetupPage(WizardAdminPage, SuperuserAdminPage):
slug = 'setup'
title = 'Setup'
label = 'Setup'
def view(self, request: HttpRequest) -> HttpResponse:
return self.render(
request=request,
wizard_steps=[
WizardStepDTO(label='Settings', url=self.get_url(), state='current', available=True, selected=True),
WizardStepDTO(label='Review', url='', state='locked', available=False)
]
)
Override admin/chassis/wizard_page.html in a project template and fill its
chassis_wizard_content block with the step form or result. The template includes
the shared stylesheet and admin/chassis/wizard_steps.html.
WizardStepDTO is frozen and has label, url, state, available and
selected=False. States are complete, error, current, ready and locked.
There must be at least one step and exactly one selected, available step; invalid
configuration raises ImproperlyConfigured. Labels and URLs are escaped.
Available steps are links; unavailable steps have no link and use
aria-disabled="true". The selected link uses aria-current="step".
Chassis renders progress. The project owns persistence, validation, transactions, idempotency and permission checks for each GET/POST and action. A locked link does not replace a server-side transition check. No session storage, form wizard dependency or domain models are added by this component.