Dashboards Overview

Developer guide for the Enezaframework dashboard system. Part of the Dashboards series.

Table of Contents

  1. What the Dashboard System Is
  2. Terminology
  3. Architecture at a Glance
  4. What a Data Source Is
  5. What a Data Source Is Not
  6. Stable Identifiers
  7. Publication Model
  8. Security Boundaries
  9. Module Entry and Navigation
  10. v1 Scope and Limitations
  11. 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:

  1. A concrete subclass of TabularReportView — including SummaryReportView and DetailedReportView — so the report already has its view, form, generator, and page.
  2. Opted in with dashboard_enabled = True.
  3. Resolvable to a Django model through report_model or get_report_model().
  4. Successfully registered in report_type_registry by being imported at startup (the abstract unified bases are skipped).
  5. Eligible after metadata parsing: its columns must 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 columns drive everything. A field that is not a report column cannot be used as a widget dimension, measure, table column, or preview column. In particular, a group_by field must also be listed in columns to 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, or DrillDownReport in 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:

python
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
  1. A dashboard is created with a draft version.
  2. Designers add tabs and widgets to the draft.
  3. Publication validates every tab, active widget, report reference, field, layout, parameter, and filter (enezadashboard/services/validation.py:79-146).
  4. Publishing marks the draft as published, points the dashboard at it, and clones it into a new draft (enezadashboard/services/publication.py:68-101).
  5. 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:

  1. Page-level module access — the user can access the business module's menu (enezadashboard/services/access.py:24-37).
  2. Page-level dashboard visibility — active, published, has a published version, module accessible, and access_code satisfied when set (enezadashboard/services/access.py:40-57).
  3. Widget-level report output permission — output.<app_label>.<report_slug>.view (enezareporting/dashboard.py:501-527).
  4. Widget-level report access function — the report view's test_func(), when present (enezareporting/dashboard.py:521-527).
  5. Widget-level report queryset and entity scope — the report's own queryset, plus dashboard_entity_scoped when the queryset supports for_entity (enezareporting/views.py).
  6. 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:

text
/dashboards/<module>/

That URL is enezadashboard:module_entry (enezadashboard/urls.py:113). ModuleEntryView selects, in order:

  1. the user's preferred dashboard for that module;
  2. the module's active default dashboard;
  3. 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_by field that is not in columns is 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_builder cannot 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:

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