Skip to main content

Custom pages

Added in 1.0.1

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.

ClassAccessGroups
PermissionAdminPagePermissionOption with a view_* codenamemay join navbar / dashboard groups
PersonalAdminPageauthenticated staffcannot join groups
SuperuserAdminPageactive superusermay 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 │
└───────────────────────────────────────────────────────────┘
Changed in 1.0.7

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 instance
  • title / sidebar_section_label from AdminSidebarService
  • any extra kwargs you pass

AdminPage attributes​

AttributeTypeDefaultPurpose
slugstrrequiredURL segment and sidebar reference
titlelocalized stringrequiredHeading
labellocalized stringrequiredNavigation label
descriptionlocalized string''Optional lead
icon_classFont Awesome class'fa-solid fa-table-list'Sidebar / groups
orderint0Sort key among pages
template_namestr'admin/chassis/page.html'Override per page
filters_fieldset_titlelocalized string_('Фильтры')If the page renders filters
chassis_settingsSettingsSettings()Same collapsed / add flags as ModelAdmin
navbar_groupAdminPageGroupOption or NoneNoneNavbar dropdown
dashboard_groupAdminPageGroupOption or NoneNoneDashboard actions section
route_prefixstr'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:

RouteName
{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:

MethodRole
view(request)list page
get_object(request, object_id) -> T | NoneNone → HTTP 404
get_object_context_data(request, object_id, obj)extra card context
object_actionslist 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.22

WizardAdminPage 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.