Cabinet Guide
When To Use It
Use cabinet when you need a reusable dashboard shell with navigation, widgets, and
typed page components. Cabinet provides the layout and structural templates; your
feature apps contribute navigation entries and build page data using typed contracts.
Installation
Add to INSTALLED_APPS and connect the context processor:
INSTALLED_APPS = [
...
"codex_django.cabinet",
"cabinet", # your project-level cabinet app
]
TEMPLATES = [{
"OPTIONS": {
"context_processors": [
...
"codex_django.cabinet.context_processors.cabinet",
]
}
}]
Register a Feature
Create cabinet.py in any installed app. CabinetConfig.ready() discovers it
automatically via autodiscover_modules("cabinet").
# features/booking/cabinet.py
from codex_django.cabinet import declare, TopbarEntry, SidebarItem, Shortcut
# Staff space
declare(
space="staff",
module="booking",
topbar=TopbarEntry(
group="services",
label="Booking",
icon="bi-calendar-check",
url="/cabinet/booking/",
order=10,
),
sidebar=[
SidebarItem(label="Schedule", url="booking:schedule", icon="bi-calendar3"),
SidebarItem(label="New Booking", url="booking:new", icon="bi-plus-circle"),
SidebarItem(label="Pending", url="booking:pending", icon="bi-hourglass-split",
badge_key="pending_count"),
],
shortcuts=[
Shortcut(label="New", url="booking:new", icon="bi-plus"),
],
)
Set the Active Module in a View
The context processor reads request.cabinet_module to know which sidebar to show.
Set it at the top of any cabinet view:
def schedule_view(request):
request.cabinet_module = "booking"
...
return render(request, "booking/schedule.html", context)
If not set, the module is auto-detected from the URL namespace (request.resolver_match.app_name),
with "admin" as the final fallback.
Use a Page Component
Build a typed contract in the view, pass it to the template, include the component.
Data Table
# view
from codex_django.cabinet import DataTableData, TableColumn, TableFilter, TableAction
def appointments_view(request):
request.cabinet_module = "booking"
table = DataTableData(
columns=[
TableColumn(key="client", label="Client", bold=True),
TableColumn(key="date", label="Date"),
TableColumn(key="status", label="Status"),
],
rows=list(Appointment.objects.values("client", "date", "status", "detail_url")),
filters=[
TableFilter(key="pending", label="Pending", value="pending"),
TableFilter(key="confirmed", label="Confirmed", value="confirmed"),
],
actions=[TableAction(label="Open", url_key="detail_url", icon="bi-eye")],
search_placeholder="Search...",
)
return render(request, "booking/appointments.html", {"table": table})
{# booking/appointments.html #}
{% extends "cabinet/base_cabinet.html" %}
{% block content %}
{% templatetag openblock %} include "cabinet/components/data_table.html" with table=table {% templatetag closeblock %}
{% templatetag openblock %} include "cabinet/includes/_modal_base.html" {% templatetag closeblock %}
{% endblock %}
Calendar Grid
Columns and rows can be anything — masters, rooms, weekdays.
The backend computes CalendarSlot.span from duration; the template only renders.
from codex_django.cabinet import CalendarGridData, CalendarSlot
# 30-minute slots from 08:00 to 20:00
rows = [f"{h}:{m:02d}" for h in range(8, 20) for m in (0, 30)]
calendar = CalendarGridData(
cols=["Master 1", "Master 2", "Master 3"],
rows=rows,
events=[
CalendarSlot(
col=0, row=2, span=2, # col index, row index, duration/slot_size
title="Ivan P.", subtitle="Haircut",
color="#6366f1", url="/booking/42/",
),
],
slot_height_px=40,
new_event_url="/booking/new/", # hx-get on empty slot click
)
return render(request, "booking/schedule.html", {"calendar": calendar})
{% extends "cabinet/base_cabinet.html" %}
{% block content %}
{% templatetag openblock %} include "cabinet/components/calendar_grid.html" with calendar=calendar {% templatetag closeblock %}
{% templatetag openblock %} include "cabinet/includes/_modal_base.html" {% templatetag closeblock %}
{% endblock %}
Card Grid
from codex_django.cabinet import CardGridData, CardItem
cards = CardGridData(
items=[
CardItem(
id=str(c.pk),
title=c.full_name,
subtitle=c.category,
avatar=c.initials,
url=f"/cabinet/clients/{c.pk}/",
meta=[("bi-telephone", c.phone)],
)
for c in Client.objects.all()
],
search_placeholder="Search clients...",
)
{% templatetag openblock %} include "cabinet/components/card_grid.html" with cards=cards {% templatetag closeblock %}
Split Panel
from codex_django.cabinet import SplitPanelData, ListRow
panel = SplitPanelData(
items=[
ListRow(id=str(c.pk), primary=c.subject, secondary=c.last_message[:60])
for c in conversations
],
detail_url="/cabinet/conversations", # appends /<id> on click
empty_message="Select a conversation",
)
{% templatetag openblock %} include "cabinet/components/split_panel.html" with panel=panel {% templatetag closeblock %}
Client Space
Register a separate declare(space="client", ...) call and extend base_client.html:
declare(
space="client",
module="booking",
sidebar=[
SidebarItem(label="My Appointments", url="booking:my_bookings",
icon="bi-calendar2-check"),
],
)
{# my_appointments.html #}
{% extends "cabinet/base_client.html" %}
{% block content %}
{% templatetag openblock %} include "cabinet/components/data_table.html" with table=table {% templatetag closeblock %}
{% endblock %}
Client-space CSS tokens are overridden independently via .cab-wrapper--client { --cab-* } in the project's theme/tokens.css.
Available Components
| Template | Contract | Use for |
|---|---|---|
cabinet/components/data_table.html |
DataTableData |
Filterable tables |
cabinet/components/calendar_grid.html |
CalendarGridData |
Scheduling grids |
cabinet/components/card_grid.html |
CardGridData |
Client/item cards |
cabinet/components/list_view.html |
ListViewData |
Simple lists |
cabinet/components/split_panel.html |
SplitPanelData |
Conversation/detail views |
Runtime Entry Points
codex_django.cabinetcodex_django.cabinet.selectorcodex_django.cabinet.redis
Related Reading
- Architecture: Cabinet Module
- API reference: Cabinet Public API
- Internals: Cabinet Internal Modules