Dashboards Overview
Developer guide for the Enezaframework dashboard system. Part of the Dashboards series.
Table of Contents
- What the Dashboard System Is
- Terminology
- Architecture at a Glance
- What a Data Source Is
- What a Data Source Is Not
- Stable Identifiers
- Publication Model
- Security Boundaries
- Module Entry and Navigation
- v1 Scope and Limitations
- Related Guides
What the Dashboard System Is
enezadashboard is a presentation layer over enezareporting. It stores a
structured dashboard (tabs and widgets), binds each widget to a registered
report, executes that report through its own view/generator, and renders the
result as server-rendered HTML.
It deliberately does not implement a second query engine. Every widget
execution goes through the report view's existing parameter form, queryset,
filters, computations, and access checks
(enezareporting/dashboard.py:1-6, enezareporting/dashboard.py:485-679).
The two jobs in this series are therefore always connected:
flowchart LR
DS[Data source<br/>tabular report] --> META[Report metadata]
META --> WIDGET[Dashboard widget]
WIDGET --> EXEC[ReportExecutionService]
EXEC --> VIEW[Report view + generator]
VIEW --> DATA[Rows, aggregates, columns]
DATA --> HTML[Server-rendered widget HTML]Expected outcome: a published dashboard whose KPI cards, charts, tables, and previews read live data from reports the current user is already allowed to see.
Terminology
| Term | Meaning | Represented by |
|---|---|---|
| Module | The Django app label that owns the dashboard. | Dashboard.module |
| Dashboard | A named composition for one module, with a stable code. | Dashboard |
| Version | An immutable published snapshot, plus a separate editable draft. | DashboardVersion |
| Tab | A named page inside a dashboard version. | DashboardTab |
| Widget | One KPI card, chart, table, or report preview inside a tab. | DashboardWidget |
| Data source | A registered, dashboard-enabled tabular report. | DashboardReportSource (in memory) |
Report reference (report_ref) |
The stable code a widget uses to find its data source. | DashboardWidget.report_ref |
| Output permission | The RBAC code that guards the report. | output.<app_label>.<report_slug>.view |
| Wireframe | A starting composition of placeholder slots. Not a permanent layout. | wireframes.py |
| Seed definition | A Python dictionary describing a dashboard. | AppConfig.dashboards |
A dashboard, its tabs, and its widgets are records, not one opaque JSON document. That is what makes validation, permissions, and version history possible.
Architecture at a Glance
flowchart TB
MODEL[Django model] --> REPORT[Tabular report class<br/>dashboard_enabled = True]
REPORT --> REGISTRY[report_type_registry<br/>auto-registered at import]
REGISTRY --> SOURCE[DashboardReportSource<br/>+ DashboardReportMeta]
SOURCE --> UI[Designer data-source list]
SOURCE --> EXEC[ReportExecutionService]
DASH[Dashboard] --> VER[DashboardVersion]
VER --> TAB[DashboardTab]
TAB --> WIDGET[DashboardWidget]
WIDGET --> EXEC
EXEC --> GEN[Report generator]
GEN --> RENDER[Widget templates]
RENDER --> PAGE[Dashboard page]The key boundary is DashboardReportSource. It exposes metadata — columns,
dimensions, measures, parameters, filter fields, permission code, URL — without
executing a report query. That metadata is what the designer uses to build field
pickers and what widget validation uses at publication time
(enezareporting/dashboard.py:50-112, enezareporting/dashboard.py:159-329).
Read Creating a Dashboard Data Source for the report side and Defining and Seeding Dashboards for the dashboard side.
What a Data Source Is
A valid dashboard data source is all of the following:
- A concrete subclass of
TabularReportView— includingSummaryReportViewandDetailedReportView— so the report already has its view, form, generator, and page. - Opted in with
dashboard_enabled = True. - Resolvable to a Django model through
report_modelorget_report_model(). - Successfully registered in
report_type_registryby being imported at startup (the abstract unified bases are skipped). - Eligible after metadata parsing: its
columnsmust parse and it must not be a crosstab, time-series, document, or drill-down report (enezareporting/dashboard.py:204-236).
When all five are true, the report appears in the designer's data-source list and can be referenced by a widget.
Important: the report's
columnsdrive everything. A field that is not a report column cannot be used as a widget dimension, measure, table column, or preview column. In particular, agroup_byfield must also be listed incolumnsto be selectable in the designer.
What a Data Source Is Not
The dashboard does not accept:
- a raw Django queryset or model, with no report class;
- a report that has not set
dashboard_enabled = True; - a
CrosstabReport,TimeSeriesReport,DocumentReport, orDrillDownReportin v1 (enezareporting/dashboard.py:205-217); - a report whose columns cannot be parsed;
- a report the current user lacks the output permission for
(
enezareporting/dashboard.py:369-384); - a saved Django admin object or an ad-hoc SQL query.
If a report is ineligible, it is silently excluded from discovery rather
than shown with an error. Discovery only returns eligible sources
(enezareporting/dashboard.py:332-355); see
Operations and Troubleshooting
when a report does not appear.
Stable Identifiers
Database primary keys are never the external identity of a dashboard definition. Use these codes:
| Object | Stable identity | Notes |
|---|---|---|
| Dashboard | (module, code) |
Unique constraint uniq_dashboard_module_code (enezadashboard/models/dashboard.py:117-127). |
| Tab | (version, code) |
Unique per version. |
| Widget | (tab, code) |
Unique per tab. |
| Data source | report_ref = <app_label>.<report_slug> by default |
dashboard_code can override it. |
| Report permission | output.<app_label>.<report_slug>.view |
Does not use dashboard_code. |
The distinction between dashboard_code and the output permission matters:
class SalesReport(TabularReportView):
report_name = "Sales"
dashboard_enabled = True
dashboard_code = "sales.v2"
With that override, widgets reference sales.v2, but the permission remains
output.<app_label>.<report_slug>.view, where report_slug is the report's slug
or lowercased class name (enezareporting/dashboard.py:178-185,
enezareporting/dashboard.py:317).
Publication Model
Viewers only ever read a published version. Designers edit a separate draft version.
stateDiagram-v2
[*] --> Draft: Dashboard created
Draft --> Published: Publish (validation passes)
Published --> Draft: Publish clones a new draft
Published --> Unpublished: Unpublish
Unpublished --> Draft: Continue editing
Published --> Draft: Restore old version as new draft- A dashboard is created with a draft version.
- Designers add tabs and widgets to the draft.
- Publication validates every tab, active widget, report reference, field,
layout, parameter, and filter (
enezadashboard/services/validation.py:79-146). - Publishing marks the draft as published, points the dashboard at it, and
clones it into a new draft (
enezadashboard/services/publication.py:68-101). - Published versions are immutable. Restoring a version creates a new draft; it
does not change the live dashboard until published
(
enezadashboard/services/publication.py:104-118).
A dashboard can be draft, published, or unpublished, and can be active or
inactive independently (enezadashboard/models/dashboard.py:17-24,
enezadashboard/models/dashboard.py:43-66).
Security Boundaries
Dashboard visibility is not data protection. The page and each widget enforce separate layers, in this order:
- Page-level module access — the user can access the business module's menu
(
enezadashboard/services/access.py:24-37). - Page-level dashboard visibility — active, published, has a published
version, module accessible, and
access_codesatisfied when set (enezadashboard/services/access.py:40-57). - Widget-level report output permission —
output.<app_label>.<report_slug>.view(enezareporting/dashboard.py:501-527). - Widget-level report access function — the report view's
test_func(), when present (enezareporting/dashboard.py:521-527). - Widget-level report queryset and entity scope — the report's own
queryset, plus
dashboard_entity_scopedwhen the queryset supportsfor_entity(enezareporting/views.py). - Widget-level validated parameters and filters — unknown parameters,
unknown filter fields, and unsupported operators are rejected
(
enezareporting/dashboard.py:529-562,enezareporting/dashboard.py:417-482).
Designer access requires the RBAC action
action.enezadashboard.manage. It is registered by
enezadashboard/apps.py:9-16 and enforced by DesignerMixin
(enezadashboard/views/designer.py:153-160).
Run python manage.py rbac_sync after deployment so RBAC discovers the action,
report outputs, and menu targets. See
Operations and Troubleshooting.
Module Entry and Navigation
Each app that defines menu_verbose_name, app_icon, and app_entry_menu
appears in the module navigation. The module's entry URL is rewritten to:
/dashboards/<module>/
That URL is enezadashboard:module_entry
(enezadashboard/urls.py:113). ModuleEntryView selects, in order:
- the user's preferred dashboard for that module;
- the module's active default dashboard;
- the first published dashboard the user can see.
If none is visible, the module shows a No dashboard designed state with
links to the module dashboard list and the create flow; it does not fall
back to the module's first list page
(enezadashboard/views/dashboard.py:39-73).
Dashboard pages set request.current_app_override so the sidebar and breadcrumb
keep showing the business module's menus rather than the dashboard app
(enezaframework/context_processors.py:45-52).
Related routes:
| URL | Name | Purpose |
|---|---|---|
/dashboards/ |
index |
All modules the user can see dashboards for. |
/dashboards/<module>/ |
module_entry |
Entry redirect / empty state. |
/dashboards/<module>/dashboards/ |
module_dashboards |
Dashboards in one module. |
/dashboards/<module>/<code>/ |
detail |
View a published dashboard. |
/dashboards/manage/ |
manage_list |
Designer list (RBAC action required). |
The full route table is in Dashboard Reference.
v1 Scope and Limitations
- Supported report types:
TabularReportView,SummaryReportView,DetailedReportView. - Not supported as data sources: crosstab, time-series, document, and drill-down reports.
- Widget types:
kpi,chart,table,report_preview. - Charts: server-rendered SVG; no client-side chart library is required.
- Dimensions: only report columns are exposed. A
group_byfield that is not incolumnsis not available. - Measures: computation fields and numeric report columns. A non-numeric field cannot be a chart value or KPI measure.
- KPI totals: true aggregates over the filtered queryset, never a sum of one table page.
- Filters: dashboard filters use the report's query-builder metadata. A
report without
use_query_buildercannot accept widget filters. - Caching: results are reused within a single request only; cross-request
caching is off by default (
enezadashboard/services/query_cache.py:7-39).
Bundled Seeded Dashboards
This repository ships two seeded dashboards:
| Dashboard | Module | Code | Tabs | Audience |
|---|---|---|---|---|
| My workspace | enezacommon |
my_workspace |
Workspace, Calendar, Chat, Workflow, Sign-ins | Every authenticated user |
| System health | enezaadminstrator |
system_health |
Overview, Security, Workflow health, Sign-ins | System administrators |
The definitions live in enezacommon/dashboards.py and
enezaadminstrator/dashboards.py. The data-source reports live in
enezacommon/reports.py and enezaadminstrator/reports.py.
Sign-in trends use LoginEvent, recorded by the user_logged_in signal in
enezacommon/signals.py. The initial migration backfills one event per user from
the existing last_login.
Deploy them with:
python manage.py migrate
python manage.py seed_dashboards --dry-run
python manage.py seed_dashboards
python manage.py rbac_sync
New dashboards are published by the seed because their definitions set
"publish": True.