Skip to content

Cabinet Internal Modules

This section contains the lower-level cabinet registry, dashboard providers, Redis managers, view helpers, and model support classes.

Registry

codex_django.cabinet.registry

In-memory registry for cabinet navigation and dashboard contributions.

Feature apps interact with this module through :func:declare. The registry stores sections, widgets, actions, sidebar items, and shortcuts for rendering via context processors and views.

Supports two APIs: - Legacy (v1): declare(module, section=CabinetSection(...)) - New (v2): declare(module, space="staff", topbar=TopbarEntry(...), sidebar=[...])

Classes

CabinetRegistry

Store cabinet contributions in process memory.

Source code in src/codex_django/cabinet/registry.py
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
class CabinetRegistry:
    """Store cabinet contributions in process memory."""

    def __init__(self) -> None:
        # Legacy v1 storage
        self._sections: dict[str, CabinetSection] = {}
        self._dashboard_widgets: list[DashboardWidget] = []
        self._topbar_actions: list[NavAction] = []
        self._global_actions: list[NavAction] = []

        # New v2 two-space storage
        # key: (space, module) → value
        self._topbar_entries: dict[str, list[TopbarEntry]] = {
            "admin": [],
            "services": [],
        }
        self._sidebar: dict[tuple[str, str], list[SidebarItem]] = {}
        self._shortcuts: dict[tuple[str, str], list[Shortcut]] = {}
        self._settings_urls: dict[tuple[str, str], str] = {}
        self._branding: dict[str, dict[str, Any]] = {}
        self._module_topbar: dict[str, TopbarEntry] = {}

    # ------------------------------------------------------------------
    # Branding & Meta
    # ------------------------------------------------------------------

    def register_branding(
        self,
        space: str,
        label: str | None = None,
        icon: str | None = None,
    ) -> None:
        """Set branding metadata for a specific space."""
        self._branding[space] = {
            "label": label,
            "icon": icon,
        }

    def get_branding(self, space: str) -> dict[str, Any]:
        """Return branding metadata for a specific space."""
        return self._branding.get(space, {})

    # ------------------------------------------------------------------
    # V2 API — two-space model
    # ------------------------------------------------------------------

    def register_v2(
        self,
        module: str,
        space: str,
        topbar: TopbarEntry | None = None,
        sidebar: list[SidebarItem] | None = None,
        shortcuts: list[Shortcut] | None = None,
        dashboard_widgets: list[DashboardWidget] | DashboardWidget | str | None = None,
        settings_url: str | None = None,
    ) -> None:
        """Register cabinet contributions for the new two-space model."""
        if topbar:
            self._module_topbar[module] = topbar
            group = topbar.group
            if group not in self._topbar_entries:
                self._topbar_entries[group] = []
            self._topbar_entries[group].append(topbar)

        if sidebar:
            self._sidebar[(space, module)] = sidebar

        if shortcuts:
            self._shortcuts[(space, module)] = shortcuts

        if dashboard_widgets:
            widgets = [dashboard_widgets] if isinstance(dashboard_widgets, DashboardWidget | str) else dashboard_widgets

            for w in widgets:
                widget = DashboardWidget(template=w) if isinstance(w, str) else w
                self._dashboard_widgets.append(widget)

        if settings_url is not None:
            self._settings_urls[(space, module)] = settings_url

    def get_topbar_entries(self, group: str | None = None) -> list[TopbarEntry]:
        """Return topbar entries, optionally filtered by group."""
        if group:
            return sorted(self._topbar_entries.get(group, []), key=lambda t: t.order)
        entries = []
        for group_entries in self._topbar_entries.values():
            entries.extend(group_entries)
        return sorted(entries, key=lambda t: (t.group, t.order))

    def get_sidebar(self, space: str, module: str) -> list[SidebarItem]:
        """Return sidebar items for a given space and module."""
        return self._sidebar.get((space, module), [])

    def get_shortcuts(self, space: str, module: str) -> list[Shortcut]:
        """Return topbar shortcuts for a given space and module."""
        return self._shortcuts.get((space, module), [])

    def get_settings_url(self, space: str, module: str) -> str | None:
        """Return the custom settings URL for a given space and module."""
        return self._settings_urls.get((space, module))

    def get_module_topbar(self, module: str) -> TopbarEntry | None:
        """Return the TopbarEntry associated with a specific module."""
        return self._module_topbar.get(module)

    # ------------------------------------------------------------------
    # V1 API — legacy (backward compatible)
    # ------------------------------------------------------------------

    def register(
        self,
        module_name: str,
        section: CabinetSection | None = None,
        dashboard_widget: DashboardWidget | str | None = None,
        topbar_actions: list[NavAction] | None = None,
        actions: list[NavAction] | None = None,
    ) -> None:
        """Register cabinet contributions (legacy API).

        .. deprecated:: Use :meth:`register_v2` with the two-space model.
        """
        if section:
            self._sections[module_name] = section

        if dashboard_widget:
            if isinstance(dashboard_widget, str):
                widget = DashboardWidget(template=dashboard_widget)
            else:
                widget = dashboard_widget
            self._dashboard_widgets.append(widget)

        if topbar_actions:
            self._topbar_actions.extend(topbar_actions)
        if actions:
            self._global_actions.extend(actions)

    @property
    def sections(self) -> list[CabinetSection]:
        """Return all registered sections ordered by their display order."""
        return sorted(self._sections.values(), key=lambda s: s.order)

    def get_sections(self, nav_group: str | None = None) -> list[CabinetSection]:
        """Return registered sections, optionally filtered by nav group."""
        sections = self.sections
        if nav_group:
            sections = [s for s in sections if s.nav_group == nav_group]
        return sections

    @property
    def dashboard_widgets(self) -> list[DashboardWidget]:
        """Return all registered dashboard widgets ordered by their display order."""
        return sorted(self._dashboard_widgets, key=lambda w: w.order)

    def get_dashboard_widgets(self, nav_group: str | None = None) -> list[DashboardWidget]:
        """Return registered dashboard widgets, optionally filtered by nav group."""
        widgets = self.dashboard_widgets
        if nav_group:
            widgets = [w for w in widgets if w.nav_group == nav_group]
        return widgets

    @property
    def topbar_actions(self) -> list[NavAction]:
        """Return topbar action declarations in registration order."""
        return self._topbar_actions

    @property
    def global_actions(self) -> list[NavAction]:
        """Return global action declarations in registration order."""
        return self._global_actions
Attributes
sections property

Return all registered sections ordered by their display order.

dashboard_widgets property

Return all registered dashboard widgets ordered by their display order.

topbar_actions property

Return topbar action declarations in registration order.

global_actions property

Return global action declarations in registration order.

Functions
register_branding(space, label=None, icon=None)

Set branding metadata for a specific space.

Source code in src/codex_django/cabinet/registry.py
52
53
54
55
56
57
58
59
60
61
62
def register_branding(
    self,
    space: str,
    label: str | None = None,
    icon: str | None = None,
) -> None:
    """Set branding metadata for a specific space."""
    self._branding[space] = {
        "label": label,
        "icon": icon,
    }
get_branding(space)

Return branding metadata for a specific space.

Source code in src/codex_django/cabinet/registry.py
64
65
66
def get_branding(self, space: str) -> dict[str, Any]:
    """Return branding metadata for a specific space."""
    return self._branding.get(space, {})
register_v2(module, space, topbar=None, sidebar=None, shortcuts=None, dashboard_widgets=None, settings_url=None)

Register cabinet contributions for the new two-space model.

Source code in src/codex_django/cabinet/registry.py
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
def register_v2(
    self,
    module: str,
    space: str,
    topbar: TopbarEntry | None = None,
    sidebar: list[SidebarItem] | None = None,
    shortcuts: list[Shortcut] | None = None,
    dashboard_widgets: list[DashboardWidget] | DashboardWidget | str | None = None,
    settings_url: str | None = None,
) -> None:
    """Register cabinet contributions for the new two-space model."""
    if topbar:
        self._module_topbar[module] = topbar
        group = topbar.group
        if group not in self._topbar_entries:
            self._topbar_entries[group] = []
        self._topbar_entries[group].append(topbar)

    if sidebar:
        self._sidebar[(space, module)] = sidebar

    if shortcuts:
        self._shortcuts[(space, module)] = shortcuts

    if dashboard_widgets:
        widgets = [dashboard_widgets] if isinstance(dashboard_widgets, DashboardWidget | str) else dashboard_widgets

        for w in widgets:
            widget = DashboardWidget(template=w) if isinstance(w, str) else w
            self._dashboard_widgets.append(widget)

    if settings_url is not None:
        self._settings_urls[(space, module)] = settings_url
get_topbar_entries(group=None)

Return topbar entries, optionally filtered by group.

Source code in src/codex_django/cabinet/registry.py
106
107
108
109
110
111
112
113
def get_topbar_entries(self, group: str | None = None) -> list[TopbarEntry]:
    """Return topbar entries, optionally filtered by group."""
    if group:
        return sorted(self._topbar_entries.get(group, []), key=lambda t: t.order)
    entries = []
    for group_entries in self._topbar_entries.values():
        entries.extend(group_entries)
    return sorted(entries, key=lambda t: (t.group, t.order))
get_sidebar(space, module)

Return sidebar items for a given space and module.

Source code in src/codex_django/cabinet/registry.py
115
116
117
def get_sidebar(self, space: str, module: str) -> list[SidebarItem]:
    """Return sidebar items for a given space and module."""
    return self._sidebar.get((space, module), [])
get_shortcuts(space, module)

Return topbar shortcuts for a given space and module.

Source code in src/codex_django/cabinet/registry.py
119
120
121
def get_shortcuts(self, space: str, module: str) -> list[Shortcut]:
    """Return topbar shortcuts for a given space and module."""
    return self._shortcuts.get((space, module), [])
get_settings_url(space, module)

Return the custom settings URL for a given space and module.

Source code in src/codex_django/cabinet/registry.py
123
124
125
def get_settings_url(self, space: str, module: str) -> str | None:
    """Return the custom settings URL for a given space and module."""
    return self._settings_urls.get((space, module))
get_module_topbar(module)

Return the TopbarEntry associated with a specific module.

Source code in src/codex_django/cabinet/registry.py
127
128
129
def get_module_topbar(self, module: str) -> TopbarEntry | None:
    """Return the TopbarEntry associated with a specific module."""
    return self._module_topbar.get(module)
register(module_name, section=None, dashboard_widget=None, topbar_actions=None, actions=None)

Register cabinet contributions (legacy API).

.. deprecated:: Use :meth:register_v2 with the two-space model.

Source code in src/codex_django/cabinet/registry.py
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
def register(
    self,
    module_name: str,
    section: CabinetSection | None = None,
    dashboard_widget: DashboardWidget | str | None = None,
    topbar_actions: list[NavAction] | None = None,
    actions: list[NavAction] | None = None,
) -> None:
    """Register cabinet contributions (legacy API).

    .. deprecated:: Use :meth:`register_v2` with the two-space model.
    """
    if section:
        self._sections[module_name] = section

    if dashboard_widget:
        if isinstance(dashboard_widget, str):
            widget = DashboardWidget(template=dashboard_widget)
        else:
            widget = dashboard_widget
        self._dashboard_widgets.append(widget)

    if topbar_actions:
        self._topbar_actions.extend(topbar_actions)
    if actions:
        self._global_actions.extend(actions)
get_sections(nav_group=None)

Return registered sections, optionally filtered by nav group.

Source code in src/codex_django/cabinet/registry.py
167
168
169
170
171
172
def get_sections(self, nav_group: str | None = None) -> list[CabinetSection]:
    """Return registered sections, optionally filtered by nav group."""
    sections = self.sections
    if nav_group:
        sections = [s for s in sections if s.nav_group == nav_group]
    return sections
get_dashboard_widgets(nav_group=None)

Return registered dashboard widgets, optionally filtered by nav group.

Source code in src/codex_django/cabinet/registry.py
179
180
181
182
183
184
def get_dashboard_widgets(self, nav_group: str | None = None) -> list[DashboardWidget]:
    """Return registered dashboard widgets, optionally filtered by nav group."""
    widgets = self.dashboard_widgets
    if nav_group:
        widgets = [w for w in widgets if w.nav_group == nav_group]
    return widgets

Functions

declare(module, space=None, topbar=None, sidebar=None, shortcuts=None, settings_url=None, dashboard_widgets=None, section=None, dashboard_widget=None, **kwargs)

Public API for cabinet.py in feature apps. Analogous to admin.site.register().

New two-space API (when space is provided)::

declare(
    module="booking",
    space="staff",
    topbar=TopbarEntry(group="services", label="Booking", ...),
    sidebar=[SidebarItem(label="Schedule", url="booking:schedule", ...)],
    shortcuts=[Shortcut(label="New", url="booking:new", icon="bi-plus")],
)

Legacy API (when space is None)::

declare(module="booking", section=CabinetSection(...))

Parameters:

Name Type Description Default
module str

Django app name or feature identifier.

required
space str | None

"staff" or "client". Activates v2 API when provided.

None
topbar TopbarEntry | None

Topbar dropdown entry (v2 only).

None
sidebar list[SidebarItem] | None

Sidebar navigation items (v2 only).

None
shortcuts list[Shortcut] | None

Topbar shortcuts for this module (v2 only).

None
section CabinetSection | None

Legacy :class:CabinetSection instance (v1 only).

None
dashboard_widgets list[DashboardWidget] | DashboardWidget | str | None

Dashboard widget declaration (v2).

None
dashboard_widget DashboardWidget | str | None

Legacy dashboard widget declaration (v1).

None
**kwargs Any

Additional keyword arguments forwarded to registry.

{}
Source code in src/codex_django/cabinet/registry.py
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
def declare(
    module: str,
    # V2 params
    space: str | None = None,
    topbar: TopbarEntry | None = None,
    sidebar: list[SidebarItem] | None = None,
    shortcuts: list[Shortcut] | None = None,
    settings_url: str | None = None,
    dashboard_widgets: list[DashboardWidget] | DashboardWidget | str | None = None,
    # V1 params (backward compat)
    section: CabinetSection | None = None,
    dashboard_widget: DashboardWidget | str | None = None,
    **kwargs: Any,
) -> None:
    """Public API for cabinet.py in feature apps. Analogous to admin.site.register().

    **New two-space API** (when ``space`` is provided)::

        declare(
            module="booking",
            space="staff",
            topbar=TopbarEntry(group="services", label="Booking", ...),
            sidebar=[SidebarItem(label="Schedule", url="booking:schedule", ...)],
            shortcuts=[Shortcut(label="New", url="booking:new", icon="bi-plus")],
        )

    **Legacy API** (when ``space`` is None)::

        declare(module="booking", section=CabinetSection(...))

    Args:
        module: Django app name or feature identifier.
        space: ``"staff"`` or ``"client"``. Activates v2 API when provided.
        topbar: Topbar dropdown entry (v2 only).
        sidebar: Sidebar navigation items (v2 only).
        shortcuts: Topbar shortcuts for this module (v2 only).
        section: Legacy :class:`CabinetSection` instance (v1 only).
        dashboard_widgets: Dashboard widget declaration (v2).
        dashboard_widget: Legacy dashboard widget declaration (v1).
        **kwargs: Additional keyword arguments forwarded to registry.
    """
    from django.core.exceptions import ImproperlyConfigured

    if space is not None:
        # V2 two-space API
        if space not in ("staff", "client"):
            raise ImproperlyConfigured(f"declare() space must be 'staff' or 'client', got '{space}'")
        cabinet_registry.register_v2(
            module=module,
            space=space,
            topbar=topbar,
            sidebar=sidebar,
            shortcuts=shortcuts,
            dashboard_widgets=dashboard_widgets or dashboard_widget,
            settings_url=settings_url,
        )
    else:
        # V1 legacy API
        if section is not None and not isinstance(section, CabinetSection):
            raise ImproperlyConfigured(
                f"cabinet.declare() section must be a CabinetSection instance, got {type(section)}"
            )
        if dashboard_widget is not None and not isinstance(dashboard_widget, DashboardWidget | str):
            raise ImproperlyConfigured(
                "cabinet.declare() dashboard_widget must be a DashboardWidget instance or string, "
                f"got {type(dashboard_widget)}"
            )
        cabinet_registry.register(
            module_name=module,
            section=section,
            dashboard_widget=dashboard_widget,
            **kwargs,
        )

configure_space(space, label=None, icon=None)

Configure space-wide metadata (branding, icon, etc).

Example::

configure_space(
    space="staff",
    label=_("Admin Panel"),
    icon="bi-shield-lock",
)
Source code in src/codex_django/cabinet/registry.py
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
def configure_space(
    space: str,
    label: str | None = None,
    icon: str | None = None,
) -> None:
    """Configure space-wide metadata (branding, icon, etc).

    Example::

        configure_space(
            space="staff",
            label=_("Admin Panel"),
            icon="bi-shield-lock",
        )
    """
    if space not in ("staff", "client"):
        from django.core.exceptions import ImproperlyConfigured

        raise ImproperlyConfigured(f"configure_space() space must be 'staff' or 'client', got '{space}'")

    cabinet_registry.register_branding(space=space, label=label, icon=icon)

Types — Navigation

codex_django.cabinet.types.nav

Navigation contracts for the cabinet two-space model.

This module defines the dataclasses that feature apps use to register navigation entries in the cabinet shell. All types here are immutable (frozen=True) to prevent accidental mutation of the global registry.

Two-space model
  • staff space (/cabinet/) — owners and administrators. Uses :class:TopbarEntry for the top navigation bar and :class:SidebarItem for the per-module sub-navigation.
  • client space (/cabinet/my/) — end-customers. Uses :class:SidebarItem only (no topbar dropdowns).

Typical usage in a feature app::

# features/booking/cabinet.py
from codex_django.cabinet import declare, TopbarEntry, SidebarItem, Shortcut

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"),
    ],
)

declare(
    space="client",
    module="booking",
    sidebar=[
        SidebarItem(label="My Appointments", url="booking:my_bookings",
                    icon="bi-calendar2-check"),
    ],
)

Classes

TopbarEntry dataclass

A navigation entry rendered inside a staff topbar dropdown group.

The staff topbar contains two dropdown menus: admin (for management pages like analytics, users, and settings) and services (for feature modules like booking, catalog, etc.). Each :class:TopbarEntry declares which dropdown it belongs to via the group field.

Entries within the same group are sorted ascending by order. This class is frozen — instances are immutable after creation.

Attributes:

Name Type Description
group str

Dropdown group identifier. Must be "admin" or "services". "admin" — Администрирование dropdown (analytics, users, settings). "services" — Сервисы dropdown (booking, store, catalog, etc.).

label str | Promise

Human-readable display name shown in the topbar link.

icon str

Bootstrap Icons class name, e.g. "bi-calendar-check".

url str

Absolute URL or named URL string resolved by the template.

order int

Sort order within the group. Lower values appear first. Defaults to 99 (end of list).

Raises:

Type Description
ImproperlyConfigured

If group is not "admin" or "services".

Example::

TopbarEntry(
    group="services",
    label="Booking",
    icon="bi-calendar-check",
    url="/cabinet/booking/",
    order=10,
)
Source code in src/codex_django/cabinet/types/nav.py
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
@dataclass(frozen=True)
class TopbarEntry:
    """A navigation entry rendered inside a staff topbar dropdown group.

    The staff topbar contains two dropdown menus: **admin** (for management
    pages like analytics, users, and settings) and **services** (for feature
    modules like booking, catalog, etc.). Each :class:`TopbarEntry` declares
    which dropdown it belongs to via the ``group`` field.

    Entries within the same group are sorted ascending by ``order``.
    This class is ``frozen`` — instances are immutable after creation.

    Attributes:
        group: Dropdown group identifier. Must be ``"admin"`` or ``"services"``.
            ``"admin"`` — Администрирование dropdown (analytics, users, settings).
            ``"services"`` — Сервисы dropdown (booking, store, catalog, etc.).
        label: Human-readable display name shown in the topbar link.
        icon: Bootstrap Icons class name, e.g. ``"bi-calendar-check"``.
        url: Absolute URL or named URL string resolved by the template.
        order: Sort order within the group. Lower values appear first.
            Defaults to ``99`` (end of list).

    Raises:
        django.core.exceptions.ImproperlyConfigured: If ``group`` is not
            ``"admin"`` or ``"services"``.

    Example::

        TopbarEntry(
            group="services",
            label="Booking",
            icon="bi-calendar-check",
            url="/cabinet/booking/",
            order=10,
        )
    """

    group: str  # "admin" | "services"
    label: str | Promise
    icon: str
    url: str
    order: int = 99

    def __post_init__(self) -> None:
        from django.core.exceptions import ImproperlyConfigured

        if self.group not in ("admin", "services"):
            raise ImproperlyConfigured(f"TopbarEntry.group must be 'admin' or 'services', got '{self.group}'")

SidebarItem dataclass

A sub-navigation link rendered in the cabinet module sidebar.

When a view sets request.cabinet_module = "booking", the context processor fetches SidebarItem list registered for that space+module combination and exposes it as cabinet_sidebar. The _sidebar_staff.html or _sidebar_client.html template then renders each item via {% include "cabinet/includes/_nav_item.html" %}.

Items are sorted ascending by order at registration time. This class is frozen — instances are immutable after creation.

Attributes:

Name Type Description
label str | Promise

Display name of the navigation link.

url str

Named URL (e.g. "booking:schedule") or absolute path. Named URLs are resolved with {% url %} in the template.

icon str

Bootstrap Icons class name, e.g. "bi-calendar3". Empty string renders no icon.

badge_key str

Context variable name to read a numeric badge count from. If the context contains { "pending_count": 3 }, set badge_key="pending_count" to show a 3 badge on this item. Empty string disables the badge.

order int

Sort order within the sidebar. Lower values appear first. Defaults to 99.

permissions tuple[str, ...]

Tuple of Django permission strings. The item is hidden unless the user has at least one of the listed permissions. Empty tuple (default) means visible to all authenticated users.

Example::

SidebarItem(
    label="Pending",
    url="booking:pending",
    icon="bi-hourglass-split",
    badge_key="pending_count",
    order=3,
    permissions=("booking.view_appointment",),
)
Source code in src/codex_django/cabinet/types/nav.py
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
@dataclass(frozen=True)
class SidebarItem:
    """A sub-navigation link rendered in the cabinet module sidebar.

    When a view sets ``request.cabinet_module = "booking"``, the context
    processor fetches ``SidebarItem`` list registered for that space+module
    combination and exposes it as ``cabinet_sidebar``. The ``_sidebar_staff.html``
    or ``_sidebar_client.html`` template then renders each item via
    ``{% include "cabinet/includes/_nav_item.html" %}``.

    Items are sorted ascending by ``order`` at registration time.
    This class is ``frozen`` — instances are immutable after creation.

    Attributes:
        label: Display name of the navigation link.
        url: Named URL (e.g. ``"booking:schedule"``) or absolute path.
            Named URLs are resolved with ``{% url %}`` in the template.
        icon: Bootstrap Icons class name, e.g. ``"bi-calendar3"``.
            Empty string renders no icon.
        badge_key: Context variable name to read a numeric badge count from.
            If the context contains ``{ "pending_count": 3 }``, set
            ``badge_key="pending_count"`` to show a ``3`` badge on this item.
            Empty string disables the badge.
        order: Sort order within the sidebar. Lower values appear first.
            Defaults to ``99``.
        permissions: Tuple of Django permission strings. The item is hidden
            unless the user has **at least one** of the listed permissions.
            Empty tuple (default) means visible to all authenticated users.

    Example::

        SidebarItem(
            label="Pending",
            url="booking:pending",
            icon="bi-hourglass-split",
            badge_key="pending_count",
            order=3,
            permissions=("booking.view_appointment",),
        )
    """

    label: str | Promise
    url: str
    icon: str = ""
    badge_key: str = ""
    order: int = 99
    permissions: tuple[str, ...] = ()

Shortcut dataclass

A quick-action link rendered in the staff topbar for the active module.

Shortcuts appear as small icon+label buttons in the topbar when the user is inside a specific cabinet module. They provide one-click access to the most common actions (e.g. "New Booking", "Add Client").

Shortcuts are registered per space+module via :func:codex_django.cabinet.declare and exposed to templates as cabinet_shortcuts.

This class is frozen — instances are immutable after creation.

Attributes:

Name Type Description
label str | Promise

Short display name, e.g. "New", "Add Client".

url str

Named URL or absolute path for the link target.

icon str

Bootstrap Icons class name, e.g. "bi-plus". Empty string renders a text-only shortcut.

Example::

Shortcut(label="New Booking", url="booking:new", icon="bi-plus-circle")
Source code in src/codex_django/cabinet/types/nav.py
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
@dataclass(frozen=True)
class Shortcut:
    """A quick-action link rendered in the staff topbar for the active module.

    Shortcuts appear as small icon+label buttons in the topbar when the user
    is inside a specific cabinet module. They provide one-click access to
    the most common actions (e.g. "New Booking", "Add Client").

    Shortcuts are registered per ``space+module`` via :func:`codex_django.cabinet.declare`
    and exposed to templates as ``cabinet_shortcuts``.

    This class is ``frozen`` — instances are immutable after creation.

    Attributes:
        label: Short display name, e.g. ``"New"``, ``"Add Client"``.
        url: Named URL or absolute path for the link target.
        icon: Bootstrap Icons class name, e.g. ``"bi-plus"``.
            Empty string renders a text-only shortcut.

    Example::

        Shortcut(label="New Booking", url="booking:new", icon="bi-plus-circle")
    """

    label: str | Promise
    url: str
    icon: str = ""

Types — Widgets

codex_django.cabinet.types.widgets

Data contracts for dashboard widget payloads.

This module defines the dataclasses that dashboard selectors return and widget templates consume. Widgets live in cabinet/templates/cabinet/widgets/ and are registered via :class:~codex_django.cabinet.types.registry.DashboardWidget.

The typical data flow is::

selector function → returns widget payload dataclass
    ↓
DashboardSelector cache layer
    ↓
context processor injects into template context
    ↓
dashboard/index.html renders {% include widget.template %}
    ↓
widget template reads typed payload from context

Widget types:

  • :class:MetricWidgetData — single KPI number with optional trend.
  • :class:TableWidgetData — tabular data with typed column definitions.
  • :class:ListWidgetData — vertical list of labelled items with optional avatars.

Supporting types:

  • :class:TableColumn — column descriptor reused by both :class:TableWidgetData (dashboard) and :class:~codex_django.cabinet.types.components.DataTableData (page component).
  • :class:ListItem — single row in a :class:ListWidgetData.

Classes

TableColumn dataclass

Descriptor for a single column in a table widget or data table component.

Shared by :class:TableWidgetData (dashboard widget) and :class:~codex_django.cabinet.types.components.DataTableData (full-page component). The template uses col.key to read the corresponding value from each row dict.

Attributes:

Name Type Description
key str

Dict key used to look up the cell value in each row. Example: "status" reads row["status"].

label str

Column header text shown to the user.

align str

Horizontal text alignment. One of "left", "center", "right". Defaults to "left".

bold bool

If True, cell text is rendered in bold. Useful for primary identifier columns (e.g. client name). Defaults to False.

muted bool

If True, cell text is rendered in muted colour (secondary information). Defaults to False.

sortable bool

If True, the column header renders a sort toggle. Sorting logic must be implemented by the view. Defaults to False.

badge_key str | None

If set, the cell value is rendered as a coloured badge. The value of this attribute is the context key that maps the cell value to a Bootstrap colour name (e.g. "status_color_map"). Defaults to None (plain text).

icon_key str | None

If set, an icon is prepended to the cell value. The value is the context key for a Bootstrap Icons class mapping. Defaults to None.

Example::

TableColumn(key="status", label="Статус", badge_key="status_color_map")
TableColumn(key="name",   label="Клиент", bold=True)
TableColumn(key="amount", label="Сумма",  align="right")
Source code in src/codex_django/cabinet/types/widgets.py
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
@dataclass
class TableColumn:
    """Descriptor for a single column in a table widget or data table component.

    Shared by :class:`TableWidgetData` (dashboard widget) and
    :class:`~codex_django.cabinet.types.components.DataTableData` (full-page
    component). The template uses ``col.key`` to read the corresponding value
    from each row dict.

    Attributes:
        key: Dict key used to look up the cell value in each row.
            Example: ``"status"`` reads ``row["status"]``.
        label: Column header text shown to the user.
        align: Horizontal text alignment. One of ``"left"``, ``"center"``,
            ``"right"``. Defaults to ``"left"``.
        bold: If ``True``, cell text is rendered in bold. Useful for primary
            identifier columns (e.g. client name). Defaults to ``False``.
        muted: If ``True``, cell text is rendered in muted colour (secondary
            information). Defaults to ``False``.
        sortable: If ``True``, the column header renders a sort toggle.
            Sorting logic must be implemented by the view. Defaults to ``False``.
        badge_key: If set, the cell value is rendered as a coloured badge.
            The value of this attribute is the context key that maps the cell
            value to a Bootstrap colour name (e.g. ``"status_color_map"``).
            Defaults to ``None`` (plain text).
        icon_key: If set, an icon is prepended to the cell value. The value
            is the context key for a Bootstrap Icons class mapping.
            Defaults to ``None``.

    Example::

        TableColumn(key="status", label="Статус", badge_key="status_color_map")
        TableColumn(key="name",   label="Клиент", bold=True)
        TableColumn(key="amount", label="Сумма",  align="right")
    """

    key: str
    label: str
    align: str = "left"
    bold: bool = False
    muted: bool = False
    sortable: bool = False
    badge_key: str | None = None
    icon_key: str | None = None

ListItem dataclass

A single row in a dashboard list widget.

Used as elements of :attr:ListWidgetData.items. Each item displays a primary label, a primary value, and optional secondary lines and avatar.

Attributes:

Name Type Description
label str

Primary descriptor on the left side, e.g. client name or service title.

value str

Primary metric on the right side, e.g. "12 000 ₽".

avatar str | None

Optional URL to an avatar image, or a 1–2 character initials string. When set, an avatar circle is rendered before the label. Defaults to None.

sublabel str | None

Optional secondary text below label, rendered smaller and muted. Defaults to None.

subvalue str | None

Optional secondary text below value. Defaults to None.

Example::

ListItem(label="Иван Петров", value="5 сеансов", avatar="ИП",
         sublabel="Постоянный клиент")
Source code in src/codex_django/cabinet/types/widgets.py
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
@dataclass
class ListItem:
    """A single row in a dashboard list widget.

    Used as elements of :attr:`ListWidgetData.items`. Each item displays
    a primary label, a primary value, and optional secondary lines and avatar.

    Attributes:
        label: Primary descriptor on the left side, e.g. client name or
            service title.
        value: Primary metric on the right side, e.g. ``"12 000 ₽"``.
        avatar: Optional URL to an avatar image, or a 1–2 character initials
            string. When set, an avatar circle is rendered before the label.
            Defaults to ``None``.
        sublabel: Optional secondary text below ``label``, rendered smaller
            and muted. Defaults to ``None``.
        subvalue: Optional secondary text below ``value``.
            Defaults to ``None``.

    Example::

        ListItem(label="Иван Петров", value="5 сеансов", avatar="ИП",
                 sublabel="Постоянный клиент")
    """

    label: str
    value: str
    avatar: str | None = None
    sublabel: str | None = None
    subvalue: str | None = None

MetricWidgetData dataclass

Payload for a KPI metric widget (cabinet/widgets/kpi.html).

Displays a single headline number with an optional trend indicator (up/down/neutral arrow + label) and a decorative icon.

Attributes:

Name Type Description
label str

Short description of the metric, e.g. "Новые клиенты".

value str

Formatted metric value, e.g. "142" or "18 500 ₽".

unit str | None

Optional unit suffix displayed after value, e.g. "₽", "%". Defaults to None.

trend_value str | None

Formatted change value, e.g. "+12%" or "−3". Displayed alongside the trend arrow. Defaults to None.

trend_label str | None

Short context for the trend, e.g. "за неделю". Defaults to None.

trend_direction str

Visual direction of the trend arrow. One of "up" (green), "down" (red), "neutral" (grey). Defaults to "neutral".

icon str | None

Bootstrap Icons class name for the decorative icon, e.g. "bi-people". Defaults to None.

Example::

MetricWidgetData(
    label="Новые клиенты",
    value="142",
    trend_value="+12%",
    trend_label="за неделю",
    trend_direction="up",
    icon="bi-people",
)
Source code in src/codex_django/cabinet/types/widgets.py
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
@dataclass
class MetricWidgetData:
    """Payload for a KPI metric widget (``cabinet/widgets/kpi.html``).

    Displays a single headline number with an optional trend indicator
    (up/down/neutral arrow + label) and a decorative icon.

    Attributes:
        label: Short description of the metric, e.g. ``"Новые клиенты"``.
        value: Formatted metric value, e.g. ``"142"`` or ``"18 500 ₽"``.
        unit: Optional unit suffix displayed after ``value``, e.g. ``"₽"``,
            ``"%"``. Defaults to ``None``.
        trend_value: Formatted change value, e.g. ``"+12%"`` or ``"−3"``.
            Displayed alongside the trend arrow. Defaults to ``None``.
        trend_label: Short context for the trend, e.g. ``"за неделю"``.
            Defaults to ``None``.
        trend_direction: Visual direction of the trend arrow. One of
            ``"up"`` (green), ``"down"`` (red), ``"neutral"`` (grey).
            Defaults to ``"neutral"``.
        icon: Bootstrap Icons class name for the decorative icon,
            e.g. ``"bi-people"``. Defaults to ``None``.

    Example::

        MetricWidgetData(
            label="Новые клиенты",
            value="142",
            trend_value="+12%",
            trend_label="за неделю",
            trend_direction="up",
            icon="bi-people",
        )
    """

    label: str
    value: str
    unit: str | None = None
    trend_value: str | None = None
    trend_label: str | None = None
    trend_direction: str = "neutral"  # "up" | "down" | "neutral"
    icon: str | None = None
    url: str | None = None

TableWidgetData dataclass

Payload for a table dashboard widget (cabinet/widgets/table.html).

Renders a compact data table inside a dashboard card. For a full-page sortable/filterable table, use :class:~codex_django.cabinet.types.components.DataTableData instead.

Attributes:

Name Type Description
columns list[TableColumn]

Ordered list of :class:TableColumn descriptors that define headers and rendering behaviour.

rows list[dict[str, Any]]

List of dicts where each key corresponds to a :attr:TableColumn.key. Extra keys are ignored by the template.

Example::

TableWidgetData(
    columns=[
        TableColumn(key="name",   label="Клиент", bold=True),
        TableColumn(key="date",   label="Дата"),
        TableColumn(key="amount", label="Сумма", align="right"),
    ],
    rows=[
        {"name": "Иван П.", "date": "01.04.2026", "amount": "3 500 ₽"},
        {"name": "Анна С.", "date": "31.03.2026", "amount": "1 200 ₽"},
    ],
)
Source code in src/codex_django/cabinet/types/widgets.py
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
@dataclass
class TableWidgetData:
    """Payload for a table dashboard widget (``cabinet/widgets/table.html``).

    Renders a compact data table inside a dashboard card. For a full-page
    sortable/filterable table, use
    :class:`~codex_django.cabinet.types.components.DataTableData` instead.

    Attributes:
        columns: Ordered list of :class:`TableColumn` descriptors that define
            headers and rendering behaviour.
        rows: List of dicts where each key corresponds to a
            :attr:`TableColumn.key`. Extra keys are ignored by the template.

    Example::

        TableWidgetData(
            columns=[
                TableColumn(key="name",   label="Клиент", bold=True),
                TableColumn(key="date",   label="Дата"),
                TableColumn(key="amount", label="Сумма", align="right"),
            ],
            rows=[
                {"name": "Иван П.", "date": "01.04.2026", "amount": "3 500 ₽"},
                {"name": "Анна С.", "date": "31.03.2026", "amount": "1 200 ₽"},
            ],
        )
    """

    columns: list[TableColumn]
    rows: list[dict[str, Any]]

ListWidgetData dataclass

Payload for a list dashboard widget (cabinet/widgets/list.html).

Renders a vertical list of labelled items with optional avatar circles inside a dashboard card. For a full-page list, use :class:~codex_django.cabinet.types.components.ListViewData instead.

Attributes:

Name Type Description
items list[ListItem]

Ordered list of :class:ListItem instances.

title str | None

Optional card title rendered above the list. Defaults to None (no title).

Example::

ListWidgetData(
    title="Топ клиенты",
    items=[
        ListItem(label="Иван Петров", value="12 000 ₽", avatar="ИП"),
        ListItem(label="Анна Смирнова", value="8 500 ₽",  avatar="АС"),
    ],
)
Source code in src/codex_django/cabinet/types/widgets.py
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
@dataclass
class ListWidgetData:
    """Payload for a list dashboard widget (``cabinet/widgets/list.html``).

    Renders a vertical list of labelled items with optional avatar circles
    inside a dashboard card. For a full-page list, use
    :class:`~codex_django.cabinet.types.components.ListViewData` instead.

    Attributes:
        items: Ordered list of :class:`ListItem` instances.
        title: Optional card title rendered above the list.
            Defaults to ``None`` (no title).

    Example::

        ListWidgetData(
            title="Топ клиенты",
            items=[
                ListItem(label="Иван Петров", value="12 000 ₽", avatar="ИП"),
                ListItem(label="Анна Смирнова", value="8 500 ₽",  avatar="АС"),
            ],
        )
    """

    items: list[ListItem]
    title: str | None = None
    subtitle: str | None = None
    icon: str | None = None

Types — Page Components

codex_django.cabinet.types.components

Data contracts for cabinet content-component templates.

Each dataclass in this module defines the payload that a template in cabinet/templates/cabinet/components/ expects to receive via Django's {% include ... with obj=obj %} tag.

Design principles:

  • Backend computes, template renders. All positioning, counts, and formatting are calculated by the view or selector before the template sees them. Templates contain no business logic.
  • One contract per component. Each include receives exactly one typed object. The template variable name matches the parameter name documented in each class.
  • Alpine.js for client-side state only. Toggle states (grid/list view, search filter) live in x-data inside the template. No custom JS files.
  • HTMX for server interactions. Detail panels, modals, and actions use hx-get / hx-post. URLs are passed through the contract.
  • Modal dispatch pattern. Components do not embed modals. They dispatch $dispatch('open-modal', {url: '...'}) and the page-level _modal_base.html handles loading.

Available components:

  • :class:DataTableDatacomponents/data_table.html
  • :class:CalendarGridData + :class:CalendarSlotcomponents/calendar_grid.html
  • :class:CardGridData + :class:CardItemcomponents/card_grid.html
  • :class:ListViewData + :class:ListRowcomponents/list_view.html
  • :class:SplitPanelData + :class:ListRowcomponents/split_panel.html

Classes

TableFilter dataclass

A single filter tab or button for :class:DataTableData.

Filter buttons are rendered as a row of toggle buttons above the table. Clicking a filter sets activeFilter in Alpine state; the table rows whose status field matches value remain visible.

Attributes:

Name Type Description
key str

Machine-readable identifier for the filter, e.g. "active".

label str

Human-readable button label, e.g. "Активные".

value str

The value to compare against the row's status field. Empty string (default) acts as "show all".

Example::

TableFilter(key="pending", label="Ожидают", value="pending")
Source code in src/codex_django/cabinet/types/components.py
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
@dataclass
class TableFilter:
    """A single filter tab or button for :class:`DataTableData`.

    Filter buttons are rendered as a row of toggle buttons above the table.
    Clicking a filter sets ``activeFilter`` in Alpine state; the table
    rows whose ``status`` field matches ``value`` remain visible.

    Attributes:
        key: Machine-readable identifier for the filter, e.g. ``"active"``.
        label: Human-readable button label, e.g. ``"Активные"``.
        value: The value to compare against the row's status field.
            Empty string (default) acts as "show all".

    Example::

        TableFilter(key="pending", label="Ожидают", value="pending")
    """

    key: str
    label: str
    value: str = ""

TableAction dataclass

A row-level action link in a data table or list component.

Each action renders as a button or link in the last column of the table (or in the row's action area). The URL for the action is read from the row dict using url_key as the lookup key.

Attributes:

Name Type Description
label str

Button label text, e.g. "Открыть", "Удалить".

url_key str

Key in the row dict whose value is used as the action URL. Example: if url_key="detail_url" and the row is {"id": 1, "detail_url": "/booking/1/"}, the action links to /booking/1/.

icon str

Bootstrap Icons class name, e.g. "bi-eye". Empty string renders a text-only button.

style str

Visual style. One of:

  • "link" — plain text link with HTMX modal open (default).
  • "btn-primary" — Bootstrap primary button.
  • "btn-danger" — Bootstrap danger button.

Example::

TableAction(label="Открыть", url_key="detail_url", icon="bi-eye")
TableAction(label="Удалить", url_key="delete_url", icon="bi-trash",
            style="btn-danger")
Source code in src/codex_django/cabinet/types/components.py
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
@dataclass
class TableAction:
    """A row-level action link in a data table or list component.

    Each action renders as a button or link in the last column of the table
    (or in the row's action area). The URL for the action is read from the
    row dict using ``url_key`` as the lookup key.

    Attributes:
        label: Button label text, e.g. ``"Открыть"``, ``"Удалить"``.
        url_key: Key in the row dict whose value is used as the action URL.
            Example: if ``url_key="detail_url"`` and the row is
            ``{"id": 1, "detail_url": "/booking/1/"}``, the action links
            to ``/booking/1/``.
        icon: Bootstrap Icons class name, e.g. ``"bi-eye"``.
            Empty string renders a text-only button.
        style: Visual style. One of:

            - ``"link"`` — plain text link with HTMX modal open (default).
            - ``"btn-primary"`` — Bootstrap primary button.
            - ``"btn-danger"`` — Bootstrap danger button.

    Example::

        TableAction(label="Открыть", url_key="detail_url", icon="bi-eye")
        TableAction(label="Удалить", url_key="delete_url", icon="bi-trash",
                    style="btn-danger")
    """

    label: str
    url_key: str
    icon: str = ""
    style: str = "link"  # "link" | "btn-primary" | "btn-danger"

DataTableData dataclass

Payload contract for components/data_table.html.

Renders a searchable, filterable data table. Search and filter state is managed client-side by Alpine.js (no page reload). Row actions open detail modals via HTMX dispatch.

Template variable: table::

{% include "cabinet/components/data_table.html" with table=table %}

Attributes:

Name Type Description
columns list[TableColumn]

Ordered list of :class:TableColumn descriptors. Each column defines a header label and how to render cell values.

rows list[dict[str, Any]]

List of dicts. Each dict must contain keys matching :attr:~TableColumn.key for every column.

filters list[TableFilter]

Optional list of :class:TableFilter instances rendered as toggle buttons above the table. Defaults to empty list (no filter bar).

actions list[TableAction]

Optional list of :class:TableAction instances rendered in the last column. Defaults to empty list (no action column).

search_placeholder str

Placeholder text for the search input. Empty string (default) hides the search bar entirely.

empty_message str

Message shown when rows is empty or all rows are filtered out. Defaults to "Нет данных".

Example::

from codex_django.cabinet import DataTableData, TableColumn, TableFilter, TableAction

table = DataTableData(
    columns=[
        TableColumn(key="name",   label="Клиент", bold=True),
        TableColumn(key="date",   label="Дата"),
        TableColumn(key="status", label="Статус", badge_key="status_colors"),
    ],
    rows=list(Appointment.objects.values("name", "date", "status", "detail_url")),
    filters=[
        TableFilter(key="pending",   label="Ожидают",    value="pending"),
        TableFilter(key="confirmed", label="Подтверждены", value="confirmed"),
    ],
    actions=[TableAction(label="Открыть", url_key="detail_url", icon="bi-eye")],
    search_placeholder="Поиск клиентов...",
)
return render(request, "booking/appointments.html", {"table": table})
Source code in src/codex_django/cabinet/types/components.py
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
@dataclass
class DataTableData:
    """Payload contract for ``components/data_table.html``.

    Renders a searchable, filterable data table. Search and filter state
    is managed client-side by Alpine.js (no page reload). Row actions open
    detail modals via HTMX dispatch.

    Template variable: ``table``::

        {% include "cabinet/components/data_table.html" with table=table %}

    Attributes:
        columns: Ordered list of :class:`TableColumn` descriptors. Each
            column defines a header label and how to render cell values.
        rows: List of dicts. Each dict must contain keys matching
            :attr:`~TableColumn.key` for every column.
        filters: Optional list of :class:`TableFilter` instances rendered
            as toggle buttons above the table. Defaults to empty list
            (no filter bar).
        actions: Optional list of :class:`TableAction` instances rendered
            in the last column. Defaults to empty list (no action column).
        search_placeholder: Placeholder text for the search input.
            Empty string (default) hides the search bar entirely.
        empty_message: Message shown when ``rows`` is empty or all rows
            are filtered out. Defaults to ``"Нет данных"``.

    Example::

        from codex_django.cabinet import DataTableData, TableColumn, TableFilter, TableAction

        table = DataTableData(
            columns=[
                TableColumn(key="name",   label="Клиент", bold=True),
                TableColumn(key="date",   label="Дата"),
                TableColumn(key="status", label="Статус", badge_key="status_colors"),
            ],
            rows=list(Appointment.objects.values("name", "date", "status", "detail_url")),
            filters=[
                TableFilter(key="pending",   label="Ожидают",    value="pending"),
                TableFilter(key="confirmed", label="Подтверждены", value="confirmed"),
            ],
            actions=[TableAction(label="Открыть", url_key="detail_url", icon="bi-eye")],
            search_placeholder="Поиск клиентов...",
        )
        return render(request, "booking/appointments.html", {"table": table})
    """

    columns: list[TableColumn]
    rows: list[dict[str, Any]]
    filters: list[TableFilter] = field(default_factory=list)
    actions: list[TableAction] = field(default_factory=list)
    search_placeholder: str = ""
    empty_message: str = "Нет данных"

CalendarSlot dataclass

A single event positioned on a :class:CalendarGridData grid.

The grid uses CSS grid-column and grid-row to place events. All positional values are zero-based indexes into :attr:CalendarGridData.cols and :attr:CalendarGridData.rows. The backend computes these values; the template only renders them.

Attributes:

Name Type Description
col int

Zero-based column index. Corresponds to the master, room, or day at that position in :attr:CalendarGridData.cols.

row int

Zero-based row index. Corresponds to the time slot at that position in :attr:CalendarGridData.rows.

span int

Number of consecutive rows the event occupies. Compute as::

span = duration_minutes // slot_size_minutes

For a 1-hour appointment with 30-minute slots: 60 // 30 = 2.

title str

Primary event text (e.g. client name or appointment type).

subtitle str

Optional secondary text (e.g. service name). Defaults to empty string.

color str

CSS color value or Bootstrap bg-* class, e.g. "#6366f1" or "bg-primary". Defaults to var(--cab-primary) if empty.

url str

HTMX endpoint to load when the event is clicked. Opens the detail/edit form in _modal_base.html. Defaults to empty string (non-clickable).

Example::

# 60-minute appointment starting at 9:00 with 30-min slots (row index 2)
CalendarSlot(
    col=0,           # first master
    row=2,           # 9:00 slot (rows: ["8:00","8:30","9:00",...])
    span=2,          # 60 min / 30 min = 2
    title="Иван П.",
    subtitle="Стрижка",
    color="#6366f1",
    url="/booking/42/",
)
Source code in src/codex_django/cabinet/types/components.py
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
@dataclass
class CalendarSlot:
    """A single event positioned on a :class:`CalendarGridData` grid.

    The grid uses CSS ``grid-column`` and ``grid-row`` to place events.
    All positional values are **zero-based indexes** into
    :attr:`CalendarGridData.cols` and :attr:`CalendarGridData.rows`.
    The backend computes these values; the template only renders them.

    Attributes:
        col: Zero-based column index. Corresponds to the master, room, or
            day at that position in :attr:`CalendarGridData.cols`.
        row: Zero-based row index. Corresponds to the time slot at that
            position in :attr:`CalendarGridData.rows`.
        span: Number of consecutive rows the event occupies. Compute as::

                span = duration_minutes // slot_size_minutes

            For a 1-hour appointment with 30-minute slots: ``60 // 30 = 2``.
        title: Primary event text (e.g. client name or appointment type).
        subtitle: Optional secondary text (e.g. service name).
            Defaults to empty string.
        color: CSS color value or Bootstrap ``bg-*`` class, e.g.
            ``"#6366f1"`` or ``"bg-primary"``. Defaults to
            ``var(--cab-primary)`` if empty.
        url: HTMX endpoint to load when the event is clicked. Opens the
            detail/edit form in ``_modal_base.html``.
            Defaults to empty string (non-clickable).

    Example::

        # 60-minute appointment starting at 9:00 with 30-min slots (row index 2)
        CalendarSlot(
            col=0,           # first master
            row=2,           # 9:00 slot (rows: ["8:00","8:30","9:00",...])
            span=2,          # 60 min / 30 min = 2
            title="Иван П.",
            subtitle="Стрижка",
            color="#6366f1",
            url="/booking/42/",
        )
    """

    col: int
    row: int
    span: int
    title: str
    subtitle: str = ""
    color: str = ""
    badge: str = ""
    badge_style: str = ""
    price: str = ""
    # Content details (e.g. list of tags or objects)
    details: list[dict[str, str]] = field(default_factory=list)
    note: str = ""
    # Icons/indicators (e.g. ["bi-chat", "bi-person-vcard"])
    indicators: list[str] = field(default_factory=list)
    url: str = ""
    left_border: str = ""
    extra: dict[str, Any] = field(default_factory=dict)

CalendarGridData dataclass

Payload contract for components/calendar_grid.html.

Renders a CSS Grid-based scheduling calendar. Columns represent any grouping axis (masters, rooms, weekdays), rows represent time slots at any granularity. The same contract is shared by two templates:

  • calendar_grid.html — vertical grid (cols × rows).
  • calendar_timeline.html — horizontal timeline (future).

The backend computes all event positions. The template renders a background grid of clickable empty slots and overlays events on top.

Template variable: calendar::

{% include "cabinet/components/calendar_grid.html" with calendar=calendar %}
{% include "cabinet/includes/_modal_base.html" %}

Attributes:

Name Type Description
cols list[str | dict[str, Any]]

Column header labels. Can be master names, room names, weekday names, or any list of strings. Example: ["Мастер 1", "Мастер 2", "Переговорная A"].

rows list[str]

Time slot labels in display order. Example: ["8:00", "8:30", "9:00", ..., "20:00"].

events list[CalendarSlot]

List of :class:CalendarSlot instances to render.

slot_height_px int

Height of each time slot row in pixels. Adjust for denser or sparser grids. Defaults to 40.

new_event_url str

HTMX base URL for creating a new event. When set, each empty slot becomes clickable and sends GET {new_event_url}?col=N&row=N. The response is loaded into _modal_base.html. Defaults to empty string (read-only).

Example::

from codex_django.cabinet import CalendarGridData, CalendarSlot

# Build 30-min slots from 8:00 to 20:00
rows = [f"{h}:{m:02d}" for h in range(8, 20) for m in (0, 30)]

calendar = CalendarGridData(
    cols=["Мастер 1", "Мастер 2", "Мастер 3"],
    rows=rows,
    events=[
        CalendarSlot(col=0, row=2, span=2,
                     title="Иван П.", subtitle="Стрижка",
                     color="#6366f1", url="/booking/42/"),
    ],
    slot_height_px=40,
    new_event_url="/booking/new/",
)
return render(request, "booking/schedule.html", {"calendar": calendar})
Source code in src/codex_django/cabinet/types/components.py
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
@dataclass
class CalendarGridData:
    """Payload contract for ``components/calendar_grid.html``.

    Renders a CSS Grid-based scheduling calendar. Columns represent any
    grouping axis (masters, rooms, weekdays), rows represent time slots at
    any granularity. The same contract is shared by two templates:

    - ``calendar_grid.html`` — vertical grid (cols × rows).
    - ``calendar_timeline.html`` — horizontal timeline (future).

    The backend computes all event positions. The template renders a
    background grid of clickable empty slots and overlays events on top.

    Template variable: ``calendar``::

        {% include "cabinet/components/calendar_grid.html" with calendar=calendar %}
        {% include "cabinet/includes/_modal_base.html" %}

    Attributes:
        cols: Column header labels. Can be master names, room names,
            weekday names, or any list of strings.
            Example: ``["Мастер 1", "Мастер 2", "Переговорная A"]``.
        rows: Time slot labels in display order.
            Example: ``["8:00", "8:30", "9:00", ..., "20:00"]``.
        events: List of :class:`CalendarSlot` instances to render.
        slot_height_px: Height of each time slot row in pixels.
            Adjust for denser or sparser grids. Defaults to ``40``.
        new_event_url: HTMX base URL for creating a new event.
            When set, each empty slot becomes clickable and sends
            ``GET {new_event_url}?col=N&row=N``. The response is loaded
            into ``_modal_base.html``. Defaults to empty string (read-only).

    Example::

        from codex_django.cabinet import CalendarGridData, CalendarSlot

        # Build 30-min slots from 8:00 to 20:00
        rows = [f"{h}:{m:02d}" for h in range(8, 20) for m in (0, 30)]

        calendar = CalendarGridData(
            cols=["Мастер 1", "Мастер 2", "Мастер 3"],
            rows=rows,
            events=[
                CalendarSlot(col=0, row=2, span=2,
                             title="Иван П.", subtitle="Стрижка",
                             color="#6366f1", url="/booking/42/"),
            ],
            slot_height_px=40,
            new_event_url="/booking/new/",
        )
        return render(request, "booking/schedule.html", {"calendar": calendar})
    """

    cols: list[str | dict[str, Any]]
    rows: list[str]
    events: list[CalendarSlot]
    slot_height_px: int = 40
    new_event_url: str = ""
    title: str = ""
    current_date: Any = None
    prev_url: str = ""
    next_url: str = ""
    today_url: str = ""
    slot_height: str = "44px"
    time_col_width: str = "60px"
    col_width: str = "1fr"

CardItem dataclass

A single card in a :class:CardGridData collection.

Renders as a card tile in grid mode or as a list row in list mode. The meta field carries icon+text pairs for secondary information (e.g. phone number, appointment count, last visit date).

Attributes:

Name Type Description
id str

Unique string identifier used as an HTML key. Should be the primary key of the underlying model, e.g. str(client.pk).

title str

Primary card heading (e.g. client full name).

subtitle str

Optional secondary line below the title (e.g. client category or role). Defaults to empty string.

avatar str

URL to an avatar image, or 1–2 character initials string for the avatar circle. Defaults to empty string (no avatar).

badge str

Short status label rendered as a badge, e.g. "VIP", "Новый". Defaults to empty string (no badge).

badge_style str

Bootstrap colour name for the badge background, e.g. "primary", "success", "secondary". Defaults to "secondary".

url str

Link target for the card. Can be a detail page URL or an HTMX endpoint. Defaults to empty string (non-clickable).

meta list[tuple[str, str]]

List of (icon, text) tuples for secondary information. Each tuple renders an icon + label pair. Example: [("bi-telephone", "+7 999 123-45-67"), ("bi-calendar", "12 визитов")].

Example::

CardItem(
    id=str(client.pk),
    title=client.full_name,
    subtitle=client.category,
    avatar=client.initials,
    badge="VIP",
    badge_style="warning",
    url=f"/cabinet/clients/{client.pk}/",
    meta=[
        ("bi-telephone", client.phone),
        ("bi-calendar",  f"{client.visit_count} визитов"),
    ],
)
Source code in src/codex_django/cabinet/types/components.py
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
@dataclass
class CardItem:
    """A single card in a :class:`CardGridData` collection.

    Renders as a card tile in grid mode or as a list row in list mode.
    The ``meta`` field carries icon+text pairs for secondary information
    (e.g. phone number, appointment count, last visit date).

    Attributes:
        id: Unique string identifier used as an HTML key. Should be the
            primary key of the underlying model, e.g. ``str(client.pk)``.
        title: Primary card heading (e.g. client full name).
        subtitle: Optional secondary line below the title
            (e.g. client category or role). Defaults to empty string.
        avatar: URL to an avatar image, or 1–2 character initials string
            for the avatar circle. Defaults to empty string (no avatar).
        badge: Short status label rendered as a badge, e.g. ``"VIP"``,
            ``"Новый"``. Defaults to empty string (no badge).
        badge_style: Bootstrap colour name for the badge background,
            e.g. ``"primary"``, ``"success"``, ``"secondary"``.
            Defaults to ``"secondary"``.
        url: Link target for the card. Can be a detail page URL or an
            HTMX endpoint. Defaults to empty string (non-clickable).
        meta: List of ``(icon, text)`` tuples for secondary information.
            Each tuple renders an icon + label pair.
            Example: ``[("bi-telephone", "+7 999 123-45-67"), ("bi-calendar", "12 визитов")]``.

    Example::

        CardItem(
            id=str(client.pk),
            title=client.full_name,
            subtitle=client.category,
            avatar=client.initials,
            badge="VIP",
            badge_style="warning",
            url=f"/cabinet/clients/{client.pk}/",
            meta=[
                ("bi-telephone", client.phone),
                ("bi-calendar",  f"{client.visit_count} визитов"),
            ],
        )
    """

    id: str
    title: str
    subtitle: str = ""
    avatar: str = ""
    badge: str = ""
    badge_style: str = "secondary"
    url: str = ""
    meta: list[tuple[str, str]] = field(default_factory=list)

CardGridData dataclass

Payload contract for components/card_grid.html.

Renders a collection of :class:CardItem instances either as a Bootstrap grid of cards or as a compact list. The user can toggle between modes client-side via Alpine.js (no page reload).

Template variable: cards::

{% include "cabinet/components/card_grid.html" with cards=cards %}

Attributes:

Name Type Description
items list[CardItem]

List of :class:CardItem instances to render.

view_mode str

Initial render mode. "grid" shows Bootstrap column cards; "list" shows compact rows. Alpine.js manages the current mode after the initial render. Defaults to "grid".

search_placeholder str

Placeholder text for the client-side search input. Alpine.js filters cards by title as the user types. Empty string (default) hides the search bar.

empty_message str

Message shown when items is empty. Defaults to "Нет элементов".

Example::

from codex_django.cabinet import CardGridData, CardItem

cards = CardGridData(
    items=[
        CardItem(
            id=str(c.pk),
            title=c.full_name,
            avatar=c.initials,
            url=f"/cabinet/clients/{c.pk}/",
        )
        for c in Client.objects.all()
    ],
    search_placeholder="Поиск клиентов...",
)
return render(request, "clients/index.html", {"cards": cards})
Source code in src/codex_django/cabinet/types/components.py
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
@dataclass
class CardGridData:
    """Payload contract for ``components/card_grid.html``.

    Renders a collection of :class:`CardItem` instances either as a
    Bootstrap grid of cards or as a compact list. The user can toggle
    between modes client-side via Alpine.js (no page reload).

    Template variable: ``cards``::

        {% include "cabinet/components/card_grid.html" with cards=cards %}

    Attributes:
        items: List of :class:`CardItem` instances to render.
        view_mode: Initial render mode. ``"grid"`` shows Bootstrap column
            cards; ``"list"`` shows compact rows. Alpine.js manages the
            current mode after the initial render.
            Defaults to ``"grid"``.
        search_placeholder: Placeholder text for the client-side search
            input. Alpine.js filters cards by ``title`` as the user types.
            Empty string (default) hides the search bar.
        empty_message: Message shown when ``items`` is empty.
            Defaults to ``"Нет элементов"``.

    Example::

        from codex_django.cabinet import CardGridData, CardItem

        cards = CardGridData(
            items=[
                CardItem(
                    id=str(c.pk),
                    title=c.full_name,
                    avatar=c.initials,
                    url=f"/cabinet/clients/{c.pk}/",
                )
                for c in Client.objects.all()
            ],
            search_placeholder="Поиск клиентов...",
        )
        return render(request, "clients/index.html", {"cards": cards})
    """

    items: list[CardItem]
    view_mode: str = "grid"  # "grid" | "list"
    search_placeholder: str = ""
    empty_message: str = "Нет элементов"

ListRow dataclass

A single row in a :class:ListViewData or :class:SplitPanelData.

Displays a primary label, optional secondary text, and optional meta information. Rows can be clickable (HTMX detail load or plain link) and carry per-row action buttons.

Attributes:

Name Type Description
id str

Unique string identifier (typically the model's primary key).

primary str

Main row text, e.g. subject line or client name.

secondary str

Optional secondary text rendered smaller below primary. Defaults to empty string.

meta str

Optional short metadata shown on the right (e.g. date, count). Defaults to empty string.

avatar str

URL or initials string for the avatar circle. Defaults to empty string (no avatar).

url str

HTMX endpoint or plain link URL. In :class:ListViewData, clicking the row dispatches open-modal. In :class:SplitPanelData, it loads the detail panel. Defaults to empty string (non-clickable).

actions list[TableAction]

Per-row :class:TableAction list. Rendered as hidden buttons that appear on hover. Defaults to empty list.

Example::

ListRow(
    id=str(msg.pk),
    primary=msg.subject,
    secondary=msg.sender,
    meta=msg.created_at.strftime("%d.%m"),
    url=f"/cabinet/conversations/{msg.pk}/",
)
Source code in src/codex_django/cabinet/types/components.py
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
@dataclass
class ListRow:
    """A single row in a :class:`ListViewData` or :class:`SplitPanelData`.

    Displays a primary label, optional secondary text, and optional meta
    information. Rows can be clickable (HTMX detail load or plain link)
    and carry per-row action buttons.

    Attributes:
        id: Unique string identifier (typically the model's primary key).
        primary: Main row text, e.g. subject line or client name.
        secondary: Optional secondary text rendered smaller below
            ``primary``. Defaults to empty string.
        meta: Optional short metadata shown on the right (e.g. date,
            count). Defaults to empty string.
        avatar: URL or initials string for the avatar circle.
            Defaults to empty string (no avatar).
        url: HTMX endpoint or plain link URL. In :class:`ListViewData`,
            clicking the row dispatches ``open-modal``. In
            :class:`SplitPanelData`, it loads the detail panel.
            Defaults to empty string (non-clickable).
        actions: Per-row :class:`TableAction` list. Rendered as hidden
            buttons that appear on hover. Defaults to empty list.

    Example::

        ListRow(
            id=str(msg.pk),
            primary=msg.subject,
            secondary=msg.sender,
            meta=msg.created_at.strftime("%d.%m"),
            url=f"/cabinet/conversations/{msg.pk}/",
        )
    """

    id: str
    primary: str
    secondary: str = ""
    meta: str = ""
    avatar: str = ""
    url: str = ""
    actions: list[TableAction] = field(default_factory=list)

ListViewData dataclass

Payload contract for components/list_view.html.

Renders a vertical list of :class:ListRow instances with an optional search bar. Clicking a row either opens a modal (HTMX) or navigates to the row's URL. Row-level action buttons appear on hover.

Template variable: list::

{% include "cabinet/components/list_view.html" with list=list_data %}
{% include "cabinet/includes/_modal_base.html" %}

Attributes:

Name Type Description
rows list[ListRow]

Ordered list of :class:ListRow instances.

search_placeholder str

Placeholder text for the Alpine.js search input. Filters rows by primary text as the user types. Empty string (default) hides the search bar.

empty_message str

Message shown when rows is empty. Defaults to "Нет данных".

Example::

from codex_django.cabinet import ListViewData, ListRow

list_data = ListViewData(
    rows=[
        ListRow(
            id=str(n.pk),
            primary=n.subject,
            secondary=n.channel,
            meta=n.sent_at.strftime("%d.%m %H:%M"),
            url=f"/cabinet/notifications/{n.pk}/",
        )
        for n in Notification.objects.order_by("-sent_at")
    ],
    search_placeholder="Поиск уведомлений...",
)
return render(request, "notifications/log.html", {"list": list_data})
Source code in src/codex_django/cabinet/types/components.py
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
@dataclass
class ListViewData:
    """Payload contract for ``components/list_view.html``.

    Renders a vertical list of :class:`ListRow` instances with an optional
    search bar. Clicking a row either opens a modal (HTMX) or navigates
    to the row's URL. Row-level action buttons appear on hover.

    Template variable: ``list``::

        {% include "cabinet/components/list_view.html" with list=list_data %}
        {% include "cabinet/includes/_modal_base.html" %}

    Attributes:
        rows: Ordered list of :class:`ListRow` instances.
        search_placeholder: Placeholder text for the Alpine.js search
            input. Filters rows by ``primary`` text as the user types.
            Empty string (default) hides the search bar.
        empty_message: Message shown when ``rows`` is empty.
            Defaults to ``"Нет данных"``.

    Example::

        from codex_django.cabinet import ListViewData, ListRow

        list_data = ListViewData(
            rows=[
                ListRow(
                    id=str(n.pk),
                    primary=n.subject,
                    secondary=n.channel,
                    meta=n.sent_at.strftime("%d.%m %H:%M"),
                    url=f"/cabinet/notifications/{n.pk}/",
                )
                for n in Notification.objects.order_by("-sent_at")
            ],
            search_placeholder="Поиск уведомлений...",
        )
        return render(request, "notifications/log.html", {"list": list_data})
    """

    rows: list[ListRow]
    search_placeholder: str = ""
    empty_message: str = "Нет данных"

SplitPanelData dataclass

Payload contract for components/split_panel.html.

Renders a two-column layout: a scrollable item list on the left and a detail area on the right. Clicking a list item loads its detail via HTMX into #split-panel-detail without a full page reload.

On mobile (< 768 px) the panels stack vertically.

Template variable: panel::

{% include "cabinet/components/split_panel.html" with panel=panel %}
Note

Place {% include "cabinet/includes/_modal_base.html" %} on the page if the detail area opens sub-modals.

Attributes:

Name Type Description
items list[ListRow]

List of :class:ListRow instances for the left panel.

active_id str

id of the initially selected item. The corresponding list row gets the cab-split-panel__item--active CSS class. If set, the initial view should also render the detail content via {% block panel_detail %} in the extending template. Defaults to empty string (no initial selection).

detail_url str

HTMX base URL for loading item details. The component appends /<item.id> and uses hx-get to load the right panel. Example: "/cabinet/conversations" → loads /cabinet/conversations/42 on click. Defaults to empty string (plain link navigation via item.url).

empty_message str

Message shown in the right panel when no item is selected. Defaults to "Выберите элемент".

Example::

from codex_django.cabinet import SplitPanelData, ListRow

panel = SplitPanelData(
    items=[
        ListRow(
            id=str(conv.pk),
            primary=conv.subject,
            secondary=conv.last_message[:60],
            meta=conv.updated_at.strftime("%d.%m"),
            avatar=conv.client_initials,
        )
        for conv in conversations
    ],
    active_id=str(active_conversation.pk) if active_conversation else "",
    detail_url="/cabinet/conversations",
    empty_message="Выберите переписку",
)
return render(request, "conversations/index.html", {"panel": panel})
Source code in src/codex_django/cabinet/types/components.py
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
@dataclass
class SplitPanelData:
    """Payload contract for ``components/split_panel.html``.

    Renders a two-column layout: a scrollable item list on the left and a
    detail area on the right. Clicking a list item loads its detail via
    HTMX into ``#split-panel-detail`` without a full page reload.

    On mobile (< 768 px) the panels stack vertically.

    Template variable: ``panel``::

        {% include "cabinet/components/split_panel.html" with panel=panel %}

    Note:
        Place ``{% include "cabinet/includes/_modal_base.html" %}`` on the
        page if the detail area opens sub-modals.

    Attributes:
        items: List of :class:`ListRow` instances for the left panel.
        active_id: ``id`` of the initially selected item. The corresponding
            list row gets the ``cab-split-panel__item--active`` CSS class.
            If set, the initial view should also render the detail content
            via ``{% block panel_detail %}`` in the extending template.
            Defaults to empty string (no initial selection).
        detail_url: HTMX base URL for loading item details. The component
            appends ``/<item.id>`` and uses ``hx-get`` to load the right
            panel. Example: ``"/cabinet/conversations"`` → loads
            ``/cabinet/conversations/42`` on click.
            Defaults to empty string (plain link navigation via ``item.url``).
        empty_message: Message shown in the right panel when no item is
            selected. Defaults to ``"Выберите элемент"``.

    Example::

        from codex_django.cabinet import SplitPanelData, ListRow

        panel = SplitPanelData(
            items=[
                ListRow(
                    id=str(conv.pk),
                    primary=conv.subject,
                    secondary=conv.last_message[:60],
                    meta=conv.updated_at.strftime("%d.%m"),
                    avatar=conv.client_initials,
                )
                for conv in conversations
            ],
            active_id=str(active_conversation.pk) if active_conversation else "",
            detail_url="/cabinet/conversations",
            empty_message="Выберите переписку",
        )
        return render(request, "conversations/index.html", {"panel": panel})
    """

    items: list[ListRow]
    active_id: str = ""
    detail_url: str = ""
    empty_message: str = "Выберите элемент"

ServiceItem dataclass

A single service or product in a selector.

Attributes:

Name Type Description
id str

Unique ID (e.g. PK).

title str

Display name (e.g. "Маникюр + Гель-лак").

price str

Price string (e.g. "45").

duration int

Duration in minutes (e.g. 90).

category str

Machine name for filtering (e.g. "nails").

Source code in src/codex_django/cabinet/types/components.py
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
@dataclass
class ServiceItem:
    """A single service or product in a selector.

    Attributes:
        id: Unique ID (e.g. PK).
        title: Display name (e.g. "Маникюр + Гель-лак").
        price: Price string (e.g. "45").
        duration: Duration in minutes (e.g. 90).
        category: Machine name for filtering (e.g. "nails").
    """

    id: str
    title: str
    price: str
    duration: int
    category: str = "all"
    description: str = ""
    master_ids: list[int] = field(default_factory=list)
    exclusive_group: str = ""
    conflicts_with: list[str] = field(default_factory=list)
    replacement_mode: str = "replace"

ServiceSelectorData dataclass

Payload for components/widgets/service_selector.html.

Attributes:

Name Type Description
items list[ServiceItem]

List of :class:ServiceItem.

categories list[tuple[str, str]]

Ordered list of (key, label) for filters.

search_placeholder str

Placeholder for the search input.

Source code in src/codex_django/cabinet/types/components.py
593
594
595
596
597
598
599
600
601
602
603
604
605
@dataclass
class ServiceSelectorData:
    """Payload for ``components/widgets/service_selector.html``.

    Attributes:
        items: List of :class:`ServiceItem`.
        categories: Ordered list of ``(key, label)`` for filters.
        search_placeholder: Placeholder for the search input.
    """

    items: list[ServiceItem]
    categories: list[tuple[str, str]] = field(default_factory=list)
    search_placeholder: str = "Search services..."

ClientSelectorData dataclass

Payload for components/widgets/client_selector.html.

Attributes:

Name Type Description
clients list[dict[str, Any]]

List of existing clients for search/selection.

search_placeholder str

Placeholder for the search input.

Source code in src/codex_django/cabinet/types/components.py
608
609
610
611
612
613
614
615
616
617
618
@dataclass
class ClientSelectorData:
    """Payload for ``components/widgets/client_selector.html``.

    Attributes:
        clients: List of existing clients for search/selection.
        search_placeholder: Placeholder for the search input.
    """

    clients: list[dict[str, Any]] = field(default_factory=list)
    search_placeholder: str = "Search by name or phone..."

DateTimePickerData dataclass

Payload for components/widgets/date_time_picker.html.

Attributes:

Name Type Description
available_days list[dict[str, Any]]

List of days to show in the mini-calendar.

time_slots list[str]

List of time strings (e.g. "08:00").

busy_slots list[str]

List of time strings that are unavailable.

current_month str

Month label (e.g. "March 2026").

Source code in src/codex_django/cabinet/types/components.py
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
@dataclass
class DateTimePickerData:
    """Payload for ``components/widgets/date_time_picker.html``.

    Attributes:
        available_days: List of days to show in the mini-calendar.
        time_slots: List of time strings (e.g. "08:00").
        busy_slots: List of time strings that are unavailable.
        current_month: Month label (e.g. "March 2026").
    """

    available_days: list[dict[str, Any]]
    time_slots: list[str]
    busy_slots: list[str] = field(default_factory=list)
    current_month: str = ""
    default_date: str = ""
    slot_matrix_json: str = "{}"

BookingSummaryData dataclass

Payload for components/widgets/summary_panel.html.

Attributes:

Name Type Description
confirm_url str

POST endpoint for the booking.

reset_url str

URL to clear the selection.

masters list[dict[str, Any]]

List of available specialists for manual assignment.

Source code in src/codex_django/cabinet/types/components.py
640
641
642
643
644
645
646
647
648
649
650
651
652
@dataclass
class BookingSummaryData:
    """Payload for ``components/widgets/summary_panel.html``.

    Attributes:
        confirm_url: POST endpoint for the booking.
        reset_url: URL to clear the selection.
        masters: List of available specialists for manual assignment.
    """

    confirm_url: str
    reset_url: str = ""
    masters: list[dict[str, Any]] = field(default_factory=list)

BookingQuickCreateServiceOption dataclass

Service option shown inside a quick-create booking modal.

Source code in src/codex_django/cabinet/types/components.py
655
656
657
658
659
660
661
662
@dataclass
class BookingQuickCreateServiceOption:
    """Service option shown inside a quick-create booking modal."""

    value: str
    label: str
    price_label: str = ""
    duration_label: str = ""

BookingQuickCreateClientOption dataclass

Client option shown inside a quick-create booking modal.

Source code in src/codex_django/cabinet/types/components.py
665
666
667
668
669
670
671
672
673
@dataclass
class BookingQuickCreateClientOption:
    """Client option shown inside a quick-create booking modal."""

    value: str
    label: str
    subtitle: str = ""
    email: str = ""
    search_text: str = ""

BookingQuickCreateData dataclass

Payload for a quick-create single-appointment block inside a modal.

Source code in src/codex_django/cabinet/types/components.py
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
@dataclass
class BookingQuickCreateData:
    """Payload for a quick-create single-appointment block inside a modal."""

    resource_label: str
    date_label: str
    time_label: str
    resource_id: str = ""
    booking_date: str = ""
    selected_time: str = ""
    service_options: list[BookingQuickCreateServiceOption] = field(default_factory=list)
    client_options: list[BookingQuickCreateClientOption] = field(default_factory=list)
    selected_service_id: str = ""
    selected_client_id: str = ""
    client_search_query: str = ""
    client_search_min_chars: int = 3
    new_client_first_name: str = ""
    new_client_last_name: str = ""
    new_client_phone: str = ""
    new_client_email: str = ""
    allow_new_client: bool = True

BookingSlotPickerOption dataclass

Single slot option in a booking slot-picker block.

Source code in src/codex_django/cabinet/types/components.py
699
700
701
702
703
704
705
@dataclass
class BookingSlotPickerOption:
    """Single slot option in a booking slot-picker block."""

    value: str
    label: str
    available: bool = True

BookingSlotPickerData dataclass

Payload for a booking-native date navigation and slot selection block.

Source code in src/codex_django/cabinet/types/components.py
708
709
710
711
712
713
714
715
716
717
718
719
@dataclass
class BookingSlotPickerData:
    """Payload for a booking-native date navigation and slot selection block."""

    selected_date: str
    selected_date_label: str
    selected_time: str = ""
    prev_url: str = ""
    next_url: str = ""
    today_url: str = ""
    calendar_url: str = ""
    slots: list[BookingSlotPickerOption] = field(default_factory=list)

BookingChainPreviewItem dataclass

Single row in a booking chain preview block.

Source code in src/codex_django/cabinet/types/components.py
722
723
724
725
726
727
728
@dataclass
class BookingChainPreviewItem:
    """Single row in a booking chain preview block."""

    title: str
    subtitle: str = ""
    meta: str = ""

BookingChainPreviewData dataclass

Payload for a booking chain preview block.

Source code in src/codex_django/cabinet/types/components.py
731
732
733
734
735
736
@dataclass
class BookingChainPreviewData:
    """Payload for a booking chain preview block."""

    title: str = "Chain preview"
    items: list[BookingChainPreviewItem] = field(default_factory=list)

ModalSection dataclass

Base class for modal sections.

Source code in src/codex_django/cabinet/types/components.py
744
745
746
747
748
@dataclass
class ModalSection:
    """Base class for modal sections."""

    type: str = "base"

ProfileSection dataclass

Bases: ModalSection

Client/Staff profile header in a modal.

Source code in src/codex_django/cabinet/types/components.py
751
752
753
754
755
756
757
758
@dataclass
class ProfileSection(ModalSection):
    """Client/Staff profile header in a modal."""

    name: str = ""
    subtitle: str = ""
    avatar: str = ""
    type: str = "profile"

SummarySection dataclass

Bases: ModalSection

Grid of key-value pairs (e.g. Appointment details).

Source code in src/codex_django/cabinet/types/components.py
767
768
769
770
771
772
@dataclass
class SummarySection(ModalSection):
    """Grid of key-value pairs (e.g. Appointment details)."""

    items: list[KeyValueItem] = field(default_factory=list)
    type: str = "summary"

FormSection dataclass

Bases: ModalSection

Input fields group.

Source code in src/codex_django/cabinet/types/components.py
785
786
787
788
789
790
@dataclass
class FormSection(ModalSection):
    """Input fields group."""

    fields: list[FormField] = field(default_factory=list)
    type: str = "form"

ModalAction dataclass

A button in the modal footer or action group.

Source code in src/codex_django/cabinet/types/components.py
793
794
795
796
797
798
799
800
801
@dataclass
class ModalAction:
    """A button in the modal footer or action group."""

    label: str
    url: str = ""
    method: str = "GET"  # "GET" | "POST" | "CLOSE"
    style: str = "btn-primary"  # "btn-primary" | "btn-secondary" | "btn-danger"
    icon: str = ""

ActionSection dataclass

Bases: ModalSection

Group of action buttons.

Source code in src/codex_django/cabinet/types/components.py
804
805
806
807
808
809
@dataclass
class ActionSection(ModalSection):
    """Group of action buttons."""

    actions: list[ModalAction] = field(default_factory=list)
    type: str = "actions"

SlotPickerSection dataclass

Bases: ModalSection

Booking-native date navigator and slot picker for modal workflows.

Source code in src/codex_django/cabinet/types/components.py
812
813
814
815
816
817
818
819
@dataclass
class SlotPickerSection(ModalSection):
    """Booking-native date navigator and slot picker for modal workflows."""

    data: BookingSlotPickerData = field(
        default_factory=lambda: BookingSlotPickerData(selected_date="", selected_date_label="")
    )
    type: str = "slot_picker"

QuickCreateSection dataclass

Bases: ModalSection

Booking quick-create block for creating one appointment from a calendar slot.

Source code in src/codex_django/cabinet/types/components.py
822
823
824
825
826
827
828
829
@dataclass
class QuickCreateSection(ModalSection):
    """Booking quick-create block for creating one appointment from a calendar slot."""

    data: BookingQuickCreateData = field(
        default_factory=lambda: BookingQuickCreateData(resource_label="", date_label="", time_label="")
    )
    type: str = "quick_create"

ChainPreviewSection dataclass

Bases: ModalSection

Booking chain preview block used by richer booking builders/modals.

Source code in src/codex_django/cabinet/types/components.py
832
833
834
835
836
837
@dataclass
class ChainPreviewSection(ModalSection):
    """Booking chain preview block used by richer booking builders/modals."""

    data: BookingChainPreviewData = field(default_factory=BookingChainPreviewData)
    type: str = "chain_preview"

ModalContentData dataclass

Payload for components/generic_modal.html.

Assembles a modal window from multiple functional sections.

Source code in src/codex_django/cabinet/types/components.py
840
841
842
843
844
845
846
847
848
@dataclass
class ModalContentData:
    """Payload for ``components/generic_modal.html``.

    Assembles a modal window from multiple functional sections.
    """

    title: str
    sections: list[ModalSection] = field(default_factory=list)

Types — Registry

codex_django.cabinet.types.registry

Registration contracts for the cabinet registry.

This module defines the dataclasses used when feature apps register dashboard widgets and navigation sections with the global :data:~codex_django.cabinet.registry.cabinet_registry.

Types:

  • :class:DashboardWidget — declares a widget to render on the dashboard.
  • :class:NavAction — a generic action link in cabinet navigation areas.
  • :class:CabinetSectiondeprecated v1 navigation section. Use :class:~codex_django.cabinet.types.nav.TopbarEntry with the new declare(space=...) API instead.

Classes

DashboardWidget dataclass

Declaration of a dashboard widget contributed by a feature app.

Feature apps register widgets via :func:~codex_django.cabinet.declare. The registry stores them and the dashboard view renders all widgets that the current user has permission to see.

The template path is resolved relative to Django's template loaders. Widget templates live in cabinet/templates/cabinet/widgets/ for built-in widgets, or in the feature app's own templates/ directory for custom widgets.

This class is frozen — instances are immutable after creation.

Attributes:

Name Type Description
template str

Django template path, e.g. "cabinet/widgets/kpi.html" or "booking/widgets/upcoming.html".

col str

Bootstrap column class controlling widget width. Defaults to "col-lg-6" (half-width on large screens). Use "col-lg-12" for full-width or "col-lg-4" for thirds.

lazy bool

If True, the widget is loaded asynchronously via HTMX after the initial page render. Useful for expensive queries. Defaults to False (eager render).

nav_group str

Navigation group this widget belongs to. Determines which dashboard tab shows the widget. Must be one of:

  • "admin" — Администрирование tab (default).
  • "services" — Сервисы tab.
  • "client" — Client portal dashboard.
permissions tuple[str, ...]

Tuple of Django permission strings. The widget is hidden unless the user has at least one listed permission. Empty tuple (default) means visible to all authenticated users.

order int

Sort order on the dashboard. Lower values appear first. Defaults to 99.

Raises:

Type Description
ImproperlyConfigured

If nav_group is not one of "admin", "services", or "client".

Example::

DashboardWidget(
    template="booking/widgets/upcoming_appointments.html",
    col="col-lg-6",
    lazy=True,
    nav_group="services",
    permissions=("booking.view_appointment",),
    order=10,
)
Source code in src/codex_django/cabinet/types/registry.py
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
@dataclass(frozen=True)
class DashboardWidget:
    """Declaration of a dashboard widget contributed by a feature app.

    Feature apps register widgets via :func:`~codex_django.cabinet.declare`.
    The registry stores them and the dashboard view renders all widgets
    that the current user has permission to see.

    The ``template`` path is resolved relative to Django's template loaders.
    Widget templates live in ``cabinet/templates/cabinet/widgets/`` for
    built-in widgets, or in the feature app's own ``templates/`` directory
    for custom widgets.

    This class is ``frozen`` — instances are immutable after creation.

    Attributes:
        template: Django template path, e.g.
            ``"cabinet/widgets/kpi.html"`` or
            ``"booking/widgets/upcoming.html"``.
        col: Bootstrap column class controlling widget width.
            Defaults to ``"col-lg-6"`` (half-width on large screens).
            Use ``"col-lg-12"`` for full-width or ``"col-lg-4"`` for thirds.
        lazy: If ``True``, the widget is loaded asynchronously via HTMX
            after the initial page render. Useful for expensive queries.
            Defaults to ``False`` (eager render).
        nav_group: Navigation group this widget belongs to. Determines
            which dashboard tab shows the widget. Must be one of:

            - ``"admin"`` — Администрирование tab (default).
            - ``"services"`` — Сервисы tab.
            - ``"client"`` — Client portal dashboard.

        permissions: Tuple of Django permission strings. The widget is
            hidden unless the user has **at least one** listed permission.
            Empty tuple (default) means visible to all authenticated users.
        order: Sort order on the dashboard. Lower values appear first.
            Defaults to ``99``.

    Raises:
        django.core.exceptions.ImproperlyConfigured: If ``nav_group`` is
            not one of ``"admin"``, ``"services"``, or ``"client"``.

    Example::

        DashboardWidget(
            template="booking/widgets/upcoming_appointments.html",
            col="col-lg-6",
            lazy=True,
            nav_group="services",
            permissions=("booking.view_appointment",),
            order=10,
        )
    """

    template: str
    context_key: str | None = None
    col: str = "col-lg-6"
    lazy: bool = False
    nav_group: str = "admin"
    permissions: tuple[str, ...] = ()
    order: int = 99

    def __post_init__(self) -> None:
        from django.core.exceptions import ImproperlyConfigured

        if self.nav_group not in ("admin", "services", "client"):
            raise ImproperlyConfigured(
                f"DashboardWidget.nav_group must be 'admin', 'services' or 'client', got '{self.nav_group}'"
            )

NavAction dataclass

A generic action link rendered in cabinet navigation areas.

Used for supplementary links that do not belong to the main topbar or sidebar navigation — for example, footer links, context-specific quick actions, or legacy v1 topbar buttons.

This class is frozen — instances are immutable after creation.

Attributes:

Name Type Description
label str

Display name of the action, e.g. "Настройки".

url str

Absolute URL or named URL string for the link.

icon str | None

Optional Bootstrap Icons class name, e.g. "bi-gear". Defaults to None (no icon).

Example::

NavAction(label="Настройки", url="/cabinet/settings/", icon="bi-gear")
Source code in src/codex_django/cabinet/types/registry.py
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
@dataclass(frozen=True)
class NavAction:
    """A generic action link rendered in cabinet navigation areas.

    Used for supplementary links that do not belong to the main topbar
    or sidebar navigation — for example, footer links, context-specific
    quick actions, or legacy v1 topbar buttons.

    This class is ``frozen`` — instances are immutable after creation.

    Attributes:
        label: Display name of the action, e.g. ``"Настройки"``.
        url: Absolute URL or named URL string for the link.
        icon: Optional Bootstrap Icons class name, e.g. ``"bi-gear"``.
            Defaults to ``None`` (no icon).

    Example::

        NavAction(label="Настройки", url="/cabinet/settings/", icon="bi-gear")
    """

    label: str
    url: str
    icon: str | None = None

CabinetSection dataclass

Deprecated. V1 navigation section for the cabinet topbar.

.. deprecated:: Use :class:~codex_django.cabinet.types.nav.TopbarEntry together with the new declare(space=...) API. CabinetSection is kept only for backward compatibility with projects generated by older versions of codex-django-cli.

In the v1 API a CabinetSection was passed directly to declare()::

# Old — do not use in new code
from codex_django.cabinet import declare, CabinetSection

declare(
    module="booking",
    section=CabinetSection(
        label="Booking",
        icon="bi-calendar",
        nav_group="services",
        url="/cabinet/booking/",
        order=10,
    ),
)

Migrate to the v2 API::

# New
from codex_django.cabinet import declare, TopbarEntry, SidebarItem

declare(
    space="staff",
    module="booking",
    topbar=TopbarEntry(
        group="services",
        label="Booking",
        icon="bi-calendar",
        url="/cabinet/booking/",
        order=10,
    ),
    sidebar=[...],
)

This class is frozen — instances are immutable after creation.

Attributes:

Name Type Description
label str

Display name of the section.

icon str

Bootstrap Icons class name, e.g. "bi-calendar".

nav_group str

Dropdown group. Must be "admin", "services", or "client". Defaults to "admin".

url str | None

Optional link URL. None is valid for dropdown-only parent sections. Defaults to None.

permissions tuple[str, ...]

Tuple of Django permission strings (OR logic). Empty tuple means visible to all. Defaults to ().

order int

Sort order. Defaults to 99.

Raises:

Type Description
ImproperlyConfigured

If nav_group is not "admin", "services", or "client".

Source code in src/codex_django/cabinet/types/registry.py
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
@dataclass(frozen=True)
class CabinetSection:
    """**Deprecated.** V1 navigation section for the cabinet topbar.

    .. deprecated::
        Use :class:`~codex_django.cabinet.types.nav.TopbarEntry` together
        with the new ``declare(space=...)`` API. ``CabinetSection`` is kept
        only for backward compatibility with projects generated by older
        versions of ``codex-django-cli``.

    In the v1 API a ``CabinetSection`` was passed directly to ``declare()``::

        # Old — do not use in new code
        from codex_django.cabinet import declare, CabinetSection

        declare(
            module="booking",
            section=CabinetSection(
                label="Booking",
                icon="bi-calendar",
                nav_group="services",
                url="/cabinet/booking/",
                order=10,
            ),
        )

    Migrate to the v2 API::

        # New
        from codex_django.cabinet import declare, TopbarEntry, SidebarItem

        declare(
            space="staff",
            module="booking",
            topbar=TopbarEntry(
                group="services",
                label="Booking",
                icon="bi-calendar",
                url="/cabinet/booking/",
                order=10,
            ),
            sidebar=[...],
        )

    This class is ``frozen`` — instances are immutable after creation.

    Attributes:
        label: Display name of the section.
        icon: Bootstrap Icons class name, e.g. ``"bi-calendar"``.
        nav_group: Dropdown group. Must be ``"admin"``, ``"services"``,
            or ``"client"``. Defaults to ``"admin"``.
        url: Optional link URL. ``None`` is valid for dropdown-only
            parent sections. Defaults to ``None``.
        permissions: Tuple of Django permission strings (OR logic).
            Empty tuple means visible to all. Defaults to ``()``.
        order: Sort order. Defaults to ``99``.

    Raises:
        django.core.exceptions.ImproperlyConfigured: If ``nav_group`` is
            not ``"admin"``, ``"services"``, or ``"client"``.
    """

    label: str
    icon: str
    nav_group: str = "admin"  # "admin" | "services" | "client"
    url: str | None = None
    permissions: tuple[str, ...] = ()
    order: int = 99

    def __post_init__(self) -> None:
        from django.core.exceptions import ImproperlyConfigured

        if self.nav_group not in ("admin", "services", "client"):
            raise ImproperlyConfigured(
                f"CabinetSection.nav_group must be 'admin', 'services' or 'client', got '{self.nav_group}'"
            )

Context processors

codex_django.cabinet.context_processors

Template context helpers for the cabinet UI shell.

Classes

Dashboard selector

codex_django.cabinet.selector.dashboard

Dashboard Selector

Single entry point for dashboard data providers. Providers are registered through @extend and cached in Redis through DashboardRedisManager using JSON rather than pickle.

Example project usage:

# cabinet/selector/dashboard.py inside the generated project
from codex_django.cabinet.selector.dashboard import DashboardSelector
from ..mock import CabinetMockData

@DashboardSelector.extend(cache_key="base_stats", cache_ttl=300)
def base_stats(request):
    return {"dashboard_stats": CabinetMockData.get_dashboard_stats()}

Adding a provider from a feature module such as booking:

# features/booking/cabinet.py
@DashboardSelector.extend(cache_key="booking_kpi", cache_ttl=300)
def booking_kpi(request):
    return {"booking_kpi": BookingSelector.get_dashboard_kpi(request)}

Invalidating cached data from a model save hook:

# features/booking/models/booking.py
from codex_django.cabinet.selector.dashboard import DashboardSelector
DashboardSelector.invalidate("booking_kpi")

Classes

DashboardAdapter

Bases: ABC

Base class for dashboard data adapters.

Adapters are responsible for fetching and formatting data for specific widget types before the selector merges them into the template context.

Source code in src/codex_django/cabinet/selector/dashboard.py
44
45
46
47
48
49
50
51
52
53
54
class DashboardAdapter(ABC):
    """Base class for dashboard data adapters.

    Adapters are responsible for fetching and formatting data for specific
    widget types before the selector merges them into the template context.
    """

    @abstractmethod
    def get_data(self, request: Any) -> dict[str, Any]:
        """Fetch and return widget data as a dictionary payload."""
        pass
Functions
get_data(request) abstractmethod

Fetch and return widget data as a dictionary payload.

Source code in src/codex_django/cabinet/selector/dashboard.py
51
52
53
54
@abstractmethod
def get_data(self, request: Any) -> dict[str, Any]:
    """Fetch and return widget data as a dictionary payload."""
    pass

MetricAdapter

Bases: DashboardAdapter

Generic adapter for metric widgets.

Source code in src/codex_django/cabinet/selector/dashboard.py
57
58
59
60
61
62
63
64
65
66
class MetricAdapter(DashboardAdapter):
    """Generic adapter for metric widgets."""

    def __init__(self, provider_fn: Callable[..., MetricWidgetData]):
        self.provider_fn = provider_fn

    def get_data(self, request: Any) -> dict[str, Any]:
        """Wrap metric payloads under the ``metric`` key."""
        data = self.provider_fn(request)
        return {"metric": data}
Functions
get_data(request)

Wrap metric payloads under the metric key.

Source code in src/codex_django/cabinet/selector/dashboard.py
63
64
65
66
def get_data(self, request: Any) -> dict[str, Any]:
    """Wrap metric payloads under the ``metric`` key."""
    data = self.provider_fn(request)
    return {"metric": data}

TableAdapter

Bases: DashboardAdapter

Generic adapter for table widgets.

Source code in src/codex_django/cabinet/selector/dashboard.py
69
70
71
72
73
74
75
76
77
78
class TableAdapter(DashboardAdapter):
    """Generic adapter for table widgets."""

    def __init__(self, provider_fn: Callable[..., TableWidgetData]):
        self.provider_fn = provider_fn

    def get_data(self, request: Any) -> dict[str, Any]:
        """Wrap table payloads under the ``table`` key."""
        data = self.provider_fn(request)
        return {"table": data}
Functions
get_data(request)

Wrap table payloads under the table key.

Source code in src/codex_django/cabinet/selector/dashboard.py
75
76
77
78
def get_data(self, request: Any) -> dict[str, Any]:
    """Wrap table payloads under the ``table`` key."""
    data = self.provider_fn(request)
    return {"table": data}

ListAdapter

Bases: DashboardAdapter

Generic adapter for list widgets.

Source code in src/codex_django/cabinet/selector/dashboard.py
81
82
83
84
85
86
87
88
89
90
class ListAdapter(DashboardAdapter):
    """Generic adapter for list widgets."""

    def __init__(self, provider_fn: Callable[..., ListWidgetData]):
        self.provider_fn = provider_fn

    def get_data(self, request: Any) -> dict[str, Any]:
        """Wrap list payloads under the ``list`` key."""
        data = self.provider_fn(request)
        return {"list": data}
Functions
get_data(request)

Wrap list payloads under the list key.

Source code in src/codex_django/cabinet/selector/dashboard.py
87
88
89
90
def get_data(self, request: Any) -> dict[str, Any]:
    """Wrap list payloads under the ``list`` key."""
    data = self.provider_fn(request)
    return {"list": data}

DashboardSelector

Extensible dashboard data aggregator with Redis caching.

Each provider is a flat function (request) -> dict or a DashboardAdapter. Registered via @DashboardSelector.extend(cache_key=..., cache_ttl=...).

Source code in src/codex_django/cabinet/selector/dashboard.py
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
class DashboardSelector:
    """
    Extensible dashboard data aggregator with Redis caching.

    Each provider is a flat function (request) -> dict or a DashboardAdapter.
    Registered via @DashboardSelector.extend(cache_key=..., cache_ttl=...).
    """

    _providers: list[dict[str, Any]] = []

    @classmethod
    def extend(
        cls,
        fn_or_adapter: Callable[..., Any] | DashboardAdapter | None = None,
        *,
        cache_key: str = "",
        cache_ttl: int = 120,
    ) -> Any:
        """
        Register a dashboard data provider.

        Args:
            cache_key:  Redis key suffix. Defaults to function/class name.
            cache_ttl:  Seconds to cache. 0 = no cache (real-time).
        """

        def decorator(obj: Callable[..., Any] | DashboardAdapter) -> Any:
            if isinstance(obj, DashboardAdapter):
                fn = obj.get_data
                name = obj.__class__.__name__.lower()
            else:
                fn = obj
                name = obj.__name__

            cls._providers.append(
                {
                    "fn": fn,
                    "cache_key": cache_key or name,
                    "cache_ttl": cache_ttl,
                }
            )
            return obj

        if fn_or_adapter is not None:
            return decorator(fn_or_adapter)
        return decorator

    @classmethod
    def get_context(cls, request: Any) -> dict[str, Any]:
        """
        Collect data from all registered providers.
        Each provider is read from Redis cache if available, otherwise called and cached.
        """
        context: dict[str, Any] = {}
        for provider in cls._providers:
            data = cls._resolve(provider, request)
            context.update(data)
        return context

    @classmethod
    def _resolve(cls, provider: dict[str, Any], request: Any) -> dict[str, Any]:
        """Resolve one provider, using Redis when caching is enabled."""
        cache_ttl = provider["cache_ttl"]
        cache_key = provider["cache_key"]

        if cache_ttl == 0:
            # Real-time: skip cache entirely
            return cast(dict[str, Any], provider["fn"](request))

        cached = _manager.get(cache_key)
        if cached is not None:
            return cached

        data = cast(dict[str, Any], provider["fn"](request))
        _manager.set(cache_key, data, ttl=cache_ttl)
        return data

    @classmethod
    def invalidate(cls, cache_key: str) -> None:
        """Invalidate cache for a specific provider. Call from model signals."""
        _manager.invalidate(cache_key)

    @classmethod
    def invalidate_all(cls) -> None:
        """Invalidate all dashboard provider caches."""
        _manager.invalidate_all()
Functions
extend(fn_or_adapter=None, *, cache_key='', cache_ttl=120) classmethod

Register a dashboard data provider.

Parameters:

Name Type Description Default
cache_key str

Redis key suffix. Defaults to function/class name.

''
cache_ttl int

Seconds to cache. 0 = no cache (real-time).

120
Source code in src/codex_django/cabinet/selector/dashboard.py
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
@classmethod
def extend(
    cls,
    fn_or_adapter: Callable[..., Any] | DashboardAdapter | None = None,
    *,
    cache_key: str = "",
    cache_ttl: int = 120,
) -> Any:
    """
    Register a dashboard data provider.

    Args:
        cache_key:  Redis key suffix. Defaults to function/class name.
        cache_ttl:  Seconds to cache. 0 = no cache (real-time).
    """

    def decorator(obj: Callable[..., Any] | DashboardAdapter) -> Any:
        if isinstance(obj, DashboardAdapter):
            fn = obj.get_data
            name = obj.__class__.__name__.lower()
        else:
            fn = obj
            name = obj.__name__

        cls._providers.append(
            {
                "fn": fn,
                "cache_key": cache_key or name,
                "cache_ttl": cache_ttl,
            }
        )
        return obj

    if fn_or_adapter is not None:
        return decorator(fn_or_adapter)
    return decorator
get_context(request) classmethod

Collect data from all registered providers. Each provider is read from Redis cache if available, otherwise called and cached.

Source code in src/codex_django/cabinet/selector/dashboard.py
140
141
142
143
144
145
146
147
148
149
150
@classmethod
def get_context(cls, request: Any) -> dict[str, Any]:
    """
    Collect data from all registered providers.
    Each provider is read from Redis cache if available, otherwise called and cached.
    """
    context: dict[str, Any] = {}
    for provider in cls._providers:
        data = cls._resolve(provider, request)
        context.update(data)
    return context
invalidate(cache_key) classmethod

Invalidate cache for a specific provider. Call from model signals.

Source code in src/codex_django/cabinet/selector/dashboard.py
170
171
172
173
@classmethod
def invalidate(cls, cache_key: str) -> None:
    """Invalidate cache for a specific provider. Call from model signals."""
    _manager.invalidate(cache_key)
invalidate_all() classmethod

Invalidate all dashboard provider caches.

Source code in src/codex_django/cabinet/selector/dashboard.py
175
176
177
178
@classmethod
def invalidate_all(cls) -> None:
    """Invalidate all dashboard provider caches."""
    _manager.invalidate_all()

Dashboard Redis manager

codex_django.cabinet.redis.managers.dashboard

Dashboard Redis Manager

Redis cache for dashboard data. Each provider is stored under its own key.

Serialization uses JSON instead of pickle, which keeps values readable from redis-cli. Supported value types are str, int, float, bool, list, dict, and Decimal converted to str.

Key format: {PROJECT_NAME}:cabinet:dashboard:{provider_key}

Classes

DashboardRedisManager

Bases: BaseDjangoRedisManager

Per-provider Redis cache for dashboard data.

Usage in DashboardSelector

_manager = DashboardRedisManager()

cached = _manager.get("booking_kpi") if cached is None: data = expensive_query() _manager.set("booking_kpi", data, ttl=300)

Invalidation from model signal / lifecycle hook: _manager.invalidate("booking_kpi") # one provider _manager.invalidate_all() # full dashboard refresh

Source code in src/codex_django/cabinet/redis/managers/dashboard.py
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
class DashboardRedisManager(BaseDjangoRedisManager):
    """
    Per-provider Redis cache for dashboard data.

    Usage in DashboardSelector:
        _manager = DashboardRedisManager()

        cached = _manager.get("booking_kpi")
        if cached is None:
            data = expensive_query()
            _manager.set("booking_kpi", data, ttl=300)

    Invalidation from model signal / lifecycle hook:
        _manager.invalidate("booking_kpi")   # one provider
        _manager.invalidate_all()            # full dashboard refresh
    """

    def __init__(self) -> None:
        super().__init__(prefix="cabinet:dashboard")

    # ── Read ──────────────────────────────────────────────────────────────────

    async def aget(self, provider_key: str) -> dict[str, Any] | None:
        """Return cached provider data or ``None`` on cache miss."""
        if self._is_disabled():
            return None
        raw = await self._client.get(self.make_key(provider_key))
        if raw is None:
            return None
        try:
            return _loads(raw)
        except (json.JSONDecodeError, ValueError):
            return None

    def get(self, provider_key: str) -> dict[str, Any] | None:
        """Synchronously return cached provider data."""
        return async_to_sync(self.aget)(provider_key)

    # ── Write ─────────────────────────────────────────────────────────────────

    async def aset(self, provider_key: str, data: dict[str, Any], ttl: int) -> None:
        """Store provider data with a TTL in seconds."""
        if self._is_disabled() or not data:
            return
        await self._client.set(
            self.make_key(provider_key),
            _dumps(data),
            ex=ttl,
        )

    def set(self, provider_key: str, data: dict[str, Any], ttl: int) -> None:
        """Synchronously store provider data with a TTL in seconds."""
        async_to_sync(self.aset)(provider_key, data, ttl)

    # ── Invalidation ──────────────────────────────────────────────────────────

    async def ainvalidate(self, provider_key: str) -> None:
        """Delete cache for a single provider."""
        if self._is_disabled():
            return
        await self._client.delete(self.make_key(provider_key))

    def invalidate(self, provider_key: str) -> None:
        """Synchronously delete cache for a single provider."""
        async_to_sync(self.ainvalidate)(provider_key)

    async def ainvalidate_all(self) -> None:
        """Delete all dashboard provider caches using a key pattern."""
        if self._is_disabled():
            return
        pattern = self.make_key("*")
        keys = await self._client.keys(pattern)
        if keys:
            await self._client.delete(*keys)

    def invalidate_all(self) -> None:
        """Synchronously delete all dashboard provider caches."""
        async_to_sync(self.ainvalidate_all)()
Functions
aget(provider_key) async

Return cached provider data or None on cache miss.

Source code in src/codex_django/cabinet/redis/managers/dashboard.py
70
71
72
73
74
75
76
77
78
79
80
async def aget(self, provider_key: str) -> dict[str, Any] | None:
    """Return cached provider data or ``None`` on cache miss."""
    if self._is_disabled():
        return None
    raw = await self._client.get(self.make_key(provider_key))
    if raw is None:
        return None
    try:
        return _loads(raw)
    except (json.JSONDecodeError, ValueError):
        return None
get(provider_key)

Synchronously return cached provider data.

Source code in src/codex_django/cabinet/redis/managers/dashboard.py
82
83
84
def get(self, provider_key: str) -> dict[str, Any] | None:
    """Synchronously return cached provider data."""
    return async_to_sync(self.aget)(provider_key)
aset(provider_key, data, ttl) async

Store provider data with a TTL in seconds.

Source code in src/codex_django/cabinet/redis/managers/dashboard.py
88
89
90
91
92
93
94
95
96
async def aset(self, provider_key: str, data: dict[str, Any], ttl: int) -> None:
    """Store provider data with a TTL in seconds."""
    if self._is_disabled() or not data:
        return
    await self._client.set(
        self.make_key(provider_key),
        _dumps(data),
        ex=ttl,
    )
set(provider_key, data, ttl)

Synchronously store provider data with a TTL in seconds.

Source code in src/codex_django/cabinet/redis/managers/dashboard.py
 98
 99
100
def set(self, provider_key: str, data: dict[str, Any], ttl: int) -> None:
    """Synchronously store provider data with a TTL in seconds."""
    async_to_sync(self.aset)(provider_key, data, ttl)
ainvalidate(provider_key) async

Delete cache for a single provider.

Source code in src/codex_django/cabinet/redis/managers/dashboard.py
104
105
106
107
108
async def ainvalidate(self, provider_key: str) -> None:
    """Delete cache for a single provider."""
    if self._is_disabled():
        return
    await self._client.delete(self.make_key(provider_key))
invalidate(provider_key)

Synchronously delete cache for a single provider.

Source code in src/codex_django/cabinet/redis/managers/dashboard.py
110
111
112
def invalidate(self, provider_key: str) -> None:
    """Synchronously delete cache for a single provider."""
    async_to_sync(self.ainvalidate)(provider_key)
ainvalidate_all() async

Delete all dashboard provider caches using a key pattern.

Source code in src/codex_django/cabinet/redis/managers/dashboard.py
114
115
116
117
118
119
120
121
async def ainvalidate_all(self) -> None:
    """Delete all dashboard provider caches using a key pattern."""
    if self._is_disabled():
        return
    pattern = self.make_key("*")
    keys = await self._client.keys(pattern)
    if keys:
        await self._client.delete(*keys)
invalidate_all()

Synchronously delete all dashboard provider caches.

Source code in src/codex_django/cabinet/redis/managers/dashboard.py
123
124
125
def invalidate_all(self) -> None:
    """Synchronously delete all dashboard provider caches."""
    async_to_sync(self.ainvalidate_all)()

Settings Redis manager

codex_django.cabinet.redis.managers.settings

Redis manager for cabinet settings payloads.

Classes

CabinetSettingsRedisManager

Bases: BaseDjangoRedisManager

Sync/async manager for CabinetSettings in Redis.

Key format: {PROJECT_NAME}:cabinet:settings Uses Redis Hash (same pattern as DjangoSiteSettingsManager). Respects DEBUG mode + CODEX_REDIS_ENABLED flag — safe for local dev without Redis.

Source code in src/codex_django/cabinet/redis/managers/settings.py
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
class CabinetSettingsRedisManager(BaseDjangoRedisManager):
    """Sync/async manager for CabinetSettings in Redis.

    Key format: {PROJECT_NAME}:cabinet:settings
    Uses Redis Hash (same pattern as DjangoSiteSettingsManager).
    Respects DEBUG mode + CODEX_REDIS_ENABLED flag — safe for local dev without Redis.
    """

    _KEY = "settings"

    def __init__(self) -> None:
        super().__init__(prefix="cabinet")

    async def aget(self) -> dict[str, Any]:
        """Asynchronously return the cached cabinet settings payload.

        Returns:
            Cached cabinet settings data, or an empty dictionary when caching
            is disabled or the Redis hash does not exist.
        """
        if self._is_disabled():
            return {}
        return await self.hash.get_all(self.make_key(self._KEY)) or {}

    def get(self) -> dict[str, Any]:
        """Synchronously return the cached cabinet settings payload."""
        return async_to_sync(self.aget)()

    async def asave_instance(self, instance: Any) -> None:
        """Asynchronously persist a cabinet settings instance to Redis.

        Args:
            instance: Object implementing ``to_cabinet_dict()``.
        """
        if self._is_disabled():
            return
        data = instance.to_cabinet_dict()
        if data:
            await self.hash.set_fields(self.make_key(self._KEY), data)

    def save_instance(self, instance: Any) -> None:
        """Synchronously persist a cabinet settings instance to Redis."""
        async_to_sync(self.asave_instance)(instance)

    async def ainvalidate(self) -> None:
        """Asynchronously invalidate the cached cabinet settings payload."""
        if self._is_disabled():
            return
        await self.string.delete(self.make_key(self._KEY))

    def invalidate(self) -> None:
        """Synchronously invalidate the cached cabinet settings payload."""
        async_to_sync(self.ainvalidate)()
Functions
aget() async

Asynchronously return the cached cabinet settings payload.

Returns:

Type Description
dict[str, Any]

Cached cabinet settings data, or an empty dictionary when caching

dict[str, Any]

is disabled or the Redis hash does not exist.

Source code in src/codex_django/cabinet/redis/managers/settings.py
23
24
25
26
27
28
29
30
31
32
async def aget(self) -> dict[str, Any]:
    """Asynchronously return the cached cabinet settings payload.

    Returns:
        Cached cabinet settings data, or an empty dictionary when caching
        is disabled or the Redis hash does not exist.
    """
    if self._is_disabled():
        return {}
    return await self.hash.get_all(self.make_key(self._KEY)) or {}
get()

Synchronously return the cached cabinet settings payload.

Source code in src/codex_django/cabinet/redis/managers/settings.py
34
35
36
def get(self) -> dict[str, Any]:
    """Synchronously return the cached cabinet settings payload."""
    return async_to_sync(self.aget)()
asave_instance(instance) async

Asynchronously persist a cabinet settings instance to Redis.

Parameters:

Name Type Description Default
instance Any

Object implementing to_cabinet_dict().

required
Source code in src/codex_django/cabinet/redis/managers/settings.py
38
39
40
41
42
43
44
45
46
47
48
async def asave_instance(self, instance: Any) -> None:
    """Asynchronously persist a cabinet settings instance to Redis.

    Args:
        instance: Object implementing ``to_cabinet_dict()``.
    """
    if self._is_disabled():
        return
    data = instance.to_cabinet_dict()
    if data:
        await self.hash.set_fields(self.make_key(self._KEY), data)
save_instance(instance)

Synchronously persist a cabinet settings instance to Redis.

Source code in src/codex_django/cabinet/redis/managers/settings.py
50
51
52
def save_instance(self, instance: Any) -> None:
    """Synchronously persist a cabinet settings instance to Redis."""
    async_to_sync(self.asave_instance)(instance)
ainvalidate() async

Asynchronously invalidate the cached cabinet settings payload.

Source code in src/codex_django/cabinet/redis/managers/settings.py
54
55
56
57
58
async def ainvalidate(self) -> None:
    """Asynchronously invalidate the cached cabinet settings payload."""
    if self._is_disabled():
        return
    await self.string.delete(self.make_key(self._KEY))
invalidate()

Synchronously invalidate the cached cabinet settings payload.

Source code in src/codex_django/cabinet/redis/managers/settings.py
60
61
62
def invalidate(self) -> None:
    """Synchronously invalidate the cached cabinet settings payload."""
    async_to_sync(self.ainvalidate)()

Settings model

codex_django.cabinet.models.settings

Singleton-like cabinet settings model and Redis synchronization hooks.

Classes

CabinetSettings

Bases: LifecycleModelMixin, Model

Cabinet settings — Singleton (always one instance, pk=1).

Stores cabinet-level configuration: name, logo, theme overrides. Auto-syncs to Redis on save via django_lifecycle hook. Skips Redis sync in DEBUG mode unless CODEX_REDIS_ENABLED=True.

Source code in src/codex_django/cabinet/models/settings.py
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
class CabinetSettings(LifecycleModelMixin, models.Model):
    """Cabinet settings — Singleton (always one instance, pk=1).

    Stores cabinet-level configuration: name, logo, theme overrides.
    Auto-syncs to Redis on save via django_lifecycle hook.
    Skips Redis sync in DEBUG mode unless CODEX_REDIS_ENABLED=True.
    """

    cabinet_name = models.CharField(_("Cabinet name"), max_length=100, default="Кабинет")
    logo = models.ImageField(_("Logo"), upload_to="cabinet/", blank=True)

    class Meta:
        verbose_name = _("Cabinet settings")
        verbose_name_plural = _("Cabinet settings")

    def __str__(self) -> str:
        """Return the display name used in admin and debug output."""
        return self.cabinet_name

    def save(self, *args: Any, **kwargs: Any) -> None:
        """Persist the singleton settings row under primary key ``1``.

        Args:
            *args: Positional arguments forwarded to ``models.Model.save()``.
            **kwargs: Keyword arguments forwarded to ``models.Model.save()``.
        """
        self.pk = 1  # Enforce Singleton
        super().save(*args, **kwargs)  # type: ignore[no-untyped-call]

    def delete(self, *args: Any, **kwargs: Any) -> tuple[int, dict[str, int]]:
        """Refuse deletion so the singleton settings row always remains available.

        Args:
            *args: Unused positional arguments accepted for API compatibility.
            **kwargs: Unused keyword arguments accepted for API compatibility.

        Returns:
            The tuple Django expects from ``delete()``, always indicating that
            no rows were removed.
        """
        return 0, {}  # Deletion is forbidden

    @classmethod
    def load(cls) -> "CabinetSettings":
        """Load or create the singleton settings instance from the database.

        Returns:
            The singleton :class:`CabinetSettings` instance.
        """
        obj, _ = cls.objects.get_or_create(pk=1)
        return obj

    def to_cabinet_dict(self) -> dict[str, Any]:
        """Serialize the model to a Redis-friendly flat mapping.

        Returns:
            A dictionary that can be stored in the cabinet settings Redis hash.
        """
        data: dict[str, Any] = {"cabinet_name": self.cabinet_name}
        if self.logo:
            try:
                data["logo"] = self.logo.url
            except ValueError:
                data["logo"] = None
        return data

    @hook(AFTER_SAVE)  # type: ignore[untyped-decorator]
    def sync_to_redis(self) -> None:
        """Persist the latest cabinet settings payload to Redis after save."""
        from django.conf import settings

        if settings.DEBUG and not getattr(settings, "CODEX_REDIS_ENABLED", False):
            return

        from ..redis.managers.settings import CabinetSettingsRedisManager

        CabinetSettingsRedisManager().save_instance(self)
Functions
__str__()

Return the display name used in admin and debug output.

Source code in src/codex_django/cabinet/models/settings.py
25
26
27
def __str__(self) -> str:
    """Return the display name used in admin and debug output."""
    return self.cabinet_name
save(*args, **kwargs)

Persist the singleton settings row under primary key 1.

Parameters:

Name Type Description Default
*args Any

Positional arguments forwarded to models.Model.save().

()
**kwargs Any

Keyword arguments forwarded to models.Model.save().

{}
Source code in src/codex_django/cabinet/models/settings.py
29
30
31
32
33
34
35
36
37
def save(self, *args: Any, **kwargs: Any) -> None:
    """Persist the singleton settings row under primary key ``1``.

    Args:
        *args: Positional arguments forwarded to ``models.Model.save()``.
        **kwargs: Keyword arguments forwarded to ``models.Model.save()``.
    """
    self.pk = 1  # Enforce Singleton
    super().save(*args, **kwargs)  # type: ignore[no-untyped-call]
delete(*args, **kwargs)

Refuse deletion so the singleton settings row always remains available.

Parameters:

Name Type Description Default
*args Any

Unused positional arguments accepted for API compatibility.

()
**kwargs Any

Unused keyword arguments accepted for API compatibility.

{}

Returns:

Type Description
int

The tuple Django expects from delete(), always indicating that

dict[str, int]

no rows were removed.

Source code in src/codex_django/cabinet/models/settings.py
39
40
41
42
43
44
45
46
47
48
49
50
def delete(self, *args: Any, **kwargs: Any) -> tuple[int, dict[str, int]]:
    """Refuse deletion so the singleton settings row always remains available.

    Args:
        *args: Unused positional arguments accepted for API compatibility.
        **kwargs: Unused keyword arguments accepted for API compatibility.

    Returns:
        The tuple Django expects from ``delete()``, always indicating that
        no rows were removed.
    """
    return 0, {}  # Deletion is forbidden
load() classmethod

Load or create the singleton settings instance from the database.

Returns:

Type Description
CabinetSettings

The singleton :class:CabinetSettings instance.

Source code in src/codex_django/cabinet/models/settings.py
52
53
54
55
56
57
58
59
60
@classmethod
def load(cls) -> "CabinetSettings":
    """Load or create the singleton settings instance from the database.

    Returns:
        The singleton :class:`CabinetSettings` instance.
    """
    obj, _ = cls.objects.get_or_create(pk=1)
    return obj
to_cabinet_dict()

Serialize the model to a Redis-friendly flat mapping.

Returns:

Type Description
dict[str, Any]

A dictionary that can be stored in the cabinet settings Redis hash.

Source code in src/codex_django/cabinet/models/settings.py
62
63
64
65
66
67
68
69
70
71
72
73
74
def to_cabinet_dict(self) -> dict[str, Any]:
    """Serialize the model to a Redis-friendly flat mapping.

    Returns:
        A dictionary that can be stored in the cabinet settings Redis hash.
    """
    data: dict[str, Any] = {"cabinet_name": self.cabinet_name}
    if self.logo:
        try:
            data["logo"] = self.logo.url
        except ValueError:
            data["logo"] = None
    return data
sync_to_redis()

Persist the latest cabinet settings payload to Redis after save.

Source code in src/codex_django/cabinet/models/settings.py
76
77
78
79
80
81
82
83
84
85
86
@hook(AFTER_SAVE)  # type: ignore[untyped-decorator]
def sync_to_redis(self) -> None:
    """Persist the latest cabinet settings payload to Redis after save."""
    from django.conf import settings

    if settings.DEBUG and not getattr(settings, "CODEX_REDIS_ENABLED", False):
        return

    from ..redis.managers.settings import CabinetSettingsRedisManager

    CabinetSettingsRedisManager().save_instance(self)

Shared model mixins

codex_django.cabinet.models.mixins

Field mixins for cabinet domain models.

These mixins define the expected interface (which fields are required), not the business logic (choices, validation). Developers declare their own status choices etc. on the concrete model.

Usage in project

from codex_django.cabinet.models import AppointmentFieldsMixin

class Appointment(AppointmentFieldsMixin): STATUS_PENDING = 'pending' STATUS_CONFIRMED = 'confirmed' STATUS_CHOICES = [(STATUS_PENDING, 'Pending'), (STATUS_CONFIRMED, 'Confirmed')] status = models.CharField(max_length=20, choices=STATUS_CHOICES, default=STATUS_PENDING)

Classes

AppointmentFieldsMixin

Bases: Model

Base fields mixin for appointments/bookings. No choices — developer defines them.

Source code in src/codex_django/cabinet/models/mixins.py
23
24
25
26
27
28
29
30
31
class AppointmentFieldsMixin(models.Model):
    """Base fields mixin for appointments/bookings. No choices — developer defines them."""

    start_at = models.DateTimeField(_("Start"))
    end_at = models.DateTimeField(_("End"))
    created_at = models.DateTimeField(_("Created"), auto_now_add=True)

    class Meta:
        abstract = True

ClientFieldsMixin

Bases: Model

Base fields mixin for client profiles.

Source code in src/codex_django/cabinet/models/mixins.py
34
35
36
37
38
39
40
41
42
43
44
45
class ClientFieldsMixin(models.Model):
    """Base fields mixin for client profiles."""

    user = models.OneToOneField(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        verbose_name=_("User"),
    )
    phone = models.CharField(_("Phone"), max_length=20, blank=True)

    class Meta:
        abstract = True

ServiceFieldsMixin

Bases: Model

Base fields mixin for services/offerings.

Source code in src/codex_django/cabinet/models/mixins.py
48
49
50
51
52
53
54
55
56
57
class ServiceFieldsMixin(models.Model):
    """Base fields mixin for services/offerings."""

    name = models.CharField(_("Name"), max_length=200)
    duration = models.PositiveIntegerField(_("Duration (minutes)"))
    price = models.DecimalField(_("Price"), max_digits=10, decimal_places=2)
    is_active = models.BooleanField(_("Active"), default=True)

    class Meta:
        abstract = True

Dashboard views

codex_django.cabinet.views.dashboard

Views for the main cabinet dashboard shell.

Classes

Functions

dashboard_view(request)

Render the dashboard page with aggregated provider context.

Parameters:

Name Type Description Default
request HttpRequest

Authenticated Django request for the cabinet dashboard.

required

Returns:

Type Description
HttpResponse

Rendered dashboard response using the registered selector providers.

Source code in src/codex_django/cabinet/views/dashboard.py
10
11
12
13
14
15
16
17
18
19
20
21
@login_required
def dashboard_view(request: HttpRequest) -> HttpResponse:
    """Render the dashboard page with aggregated provider context.

    Args:
        request: Authenticated Django request for the cabinet dashboard.

    Returns:
        Rendered dashboard response using the registered selector providers.
    """
    context = DashboardSelector.get_context(request)
    return render(request, "cabinet/dashboard/index.html", {"dashboard_data": context, **context})

Site settings views

codex_django.cabinet.views.site_settings

Views for cabinet site-settings pages and HTMX partials.

Functions

site_settings_view(request)

Отображает единую страницу всех настроек сайта и обрабатывает сохранение.

Source code in src/codex_django/cabinet/views/site_settings.py
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@login_required
def site_settings_view(request: HttpRequest) -> HttpResponse:
    """Отображает единую страницу всех настроек сайта и обрабатывает сохранение."""
    from django.contrib import messages

    if request.method == "POST":
        success, msg = SiteSettingsService.save_context(request)
        if success:
            messages.success(request, msg)
        else:
            messages.error(request, msg)

    context = SiteSettingsService.get_context(request)
    return render(request, "cabinet/site_settings/index.html", context)

site_settings_tab_view(request, tab_slug)

Устаревшая вьюха для вкладок, теперь просто редиректит на общий корень по якорю.

Source code in src/codex_django/cabinet/views/site_settings.py
27
28
29
30
@login_required
def site_settings_tab_view(request: HttpRequest, tab_slug: str) -> HttpResponse:
    """Устаревшая вьюха для вкладок, теперь просто редиректит на общий корень по якорю."""
    return redirect(f"{reverse('cabinet:site_settings')}#{tab_slug}")