Creating a Dashboard Data Source

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

Table of Contents

  1. Overview
  2. Prerequisites
  3. Step 1 — Choose the Report Type
  4. Step 2 — Declare the Report Class
  5. Step 3 — Opt In to Dashboards
  6. Step 4 — Make Sure the Class Is Registered
  7. Step 5 — Expose Dimensions and Measures
  8. Step 6 — Enable Filters (Optional)
  9. Step 7 — Grant Permissions
  10. Step 8 — Verify the Data Source
  11. Code and Permission Derivation
  12. Eligibility Rules
  13. What the Dashboard Sees
  14. Common Mistakes
  15. Completion Checklist
  16. Related Guides

Overview

A dashboard data source is not a new model. It is an existing tabular report that has opted in to dashboard use:

text
Django model → tabular report class → dashboard_enabled = True → eligible source

The dashboard layer reads the report's metadata and later executes the report through its own view, so the report remains the single source of truth for columns, calculations, filters, and access checks (enezareporting/dashboard.py:1-6).

You will build: a report class over a model, opt it in, expose a few dimensions and measures, and confirm it appears in the designer.


Prerequisites

Before you start, confirm:

  • [ ] The model exists, has fields with meaningful verbose_names, and is migrated.
  • [ ] The report class lives in an imported module, usually myapp/views/reports.py re-exported by myapp/views/__init__.py.
  • [ ] The report renders at a URL if you want the widget's View report link to work.
  • [ ] You know which field is the report's date field.
  • [ ] You know which fields are dimensions (categories) and which are measures (numeric values).

The frameworkapp reference implementation is in frameworkapp/views/report.py:13-55. Its end-to-end walkthrough is in Worked Example: frameworkapp.


Step 1 — Choose the Report Type

Use a tabular report:

Type Use when
TabularReportView Grouped rows with optional aggregates. The normal choice.
SummaryReportView Aggregates only; no detail rows.
DetailedReportView Every record; no aggregation.

Each base already includes the form, generator, renderable page, designer rendering, query-builder filters, and totals hooks. Inherit exactly one:

python
class SalesReport(TabularReportView):
    ...

Set design_slug on the class when you need the report designer's template behavior; no DesignTemplateMixin is needed:

python
class FrameworktestdocumentSales(TabularReportView):
    design_slug = "frameworkappframeworktestdocumentline"
    ...

Do not use CrosstabReportView, TimeSeriesReportView, DocumentReportView, or DrillDownReportView — they are not dashboard data sources in v1 (enezareporting/dashboard.py:205-217).


Step 2 — Declare the Report Class

A report has three kinds of configuration: the model it reads, the columns it exposes, and the parameters it accepts.

python
# myapp/views/reports.py
from django.db.models import Sum

from enezareporting.fields import ComputationField
from enezareporting.parameters import DateRangeParam, ParameterSet
from enezareporting.report_base import TabularReportView

from myapp.models import SalesLine


class SalesByItem(TabularReportView):
    report_name = "Sales by Item"
    report_model = SalesLine
    date_field = "date_placed"
    group_by = "item_code", "item_description"

    TotalQuantity = ComputationField.create(
        Sum,
        "quantity",
        verbose_name="Total Quantity",
    )
    TotalValue = ComputationField.create(
        Sum,
        "value",
        verbose_name="Total Value",
    )

    columns = [
        "item_code",
        "item_description",
        TotalQuantity,
        TotalValue,
    ]

    parameter_spec = ParameterSet(
        DateRangeParam(name="date_range", start_field="date_placed"),
    )

Key attributes:

Attribute Required Effect
report_model Yes The model the report reads. Can be supplied by get_report_model() instead.
columns Yes Fields and computation fields exposed to reports and dashboards.
group_by No Fields the report groups by. A grouped field must also be in columns to be a widget dimension.
date_field No Date field used for date-range filtering and dashboard periods.
report_name Recommended Human title in the designer. Falls back to report_title and then the class name.
parameter_spec Recommended Filter inputs. See Step 6.
report_generator_class Optional Generator override, for example ListViewReportGenerator.

The full attribute list is in Dashboard Reference — Report class attributes.


Step 3 — Opt In to Dashboards

Add the opt-in flag and the optional dashboard attributes:

python
class SalesByItem(TabularReportView):
    report_name = "Sales by Item"
    dashboard_enabled = True
    dashboard_entity_scoped = True
    report_url_name = "myapp:sales_by_item"
    ...
Attribute Required Effect
dashboard_enabled Yes Without it the report is never a data source.
dashboard_code No Stable override for report_ref. Defaults to <app_label>.<report_slug>.
report_url_name No URL name used by the widget's View report link.
dashboard_entity_scoped No When true, dashboard execution calls queryset.for_entity(request=request) if the entity and method exist.

dashboard_entity_scoped only has an effect when:

  • the request has current_entity, and
  • the report queryset defines for_entity (enezareporting/views.py).

Without it, the report's normal queryset is used unchanged.


Step 4 — Make Sure the Class Is Registered

Report classes register themselves automatically when Python defines them, as long as they are concrete (not __abstract__, not prefixed with _). The unified bases are abstract and are skipped (enezareporting/report_types.py, enezareporting/report_base.py).

That means the module must be imported at startup. The standard ways are:

  1. myapp/views/__init__.py imports the report module (frameworkapp does this at frameworkapp/views/__init__.py:1).
  2. myapp/urls.py imports the class to build a path(...).

If neither happens, the class never registers and no dashboard can see it.

python
# myapp/views/__init__.py
from .reports import *  # noqa: F401,F403

After changing report code in a running process, clear the discovery cache so the next designer request rebuilds metadata:

python
from enezareporting.dashboard import clear_dashboard_report_cache

clear_dashboard_report_cache()

The cache is process-local and is described in Step 8.


Step 5 — Expose Dimensions and Measures

The dashboard derives everything from parsed report columns (enezareporting/dashboard.py:219-278):

Concept Comes from Use
Column Every entry in columns Tables, previews, chart categories if the source is a database field.
Dimension A column whose parsed source is database Chart category_field, series fields, table columns.
Measure A computation field, or a numeric model field KPI measure, chart value_field, gauge measure, bubble size.

Rules to remember:

  1. A group_by field must also be a report column to appear as a dimension. If group_by = "item_code" but "item_code" is not in columns, the designer cannot offer it.
  2. Computation fields become measures automatically. Their canonical name is <method>__<field>, for example sum__item_quantity (enezareporting/fields.py:76-99). Use that canonical name in widget configuration.
  3. Numeric model fields can also be used as measures and support sum, avg, min, max, and count (enezareporting/dashboard.py:147-156).
  4. Non-numeric fields cannot be measures. A chart value field must be one of meta.measures (enezadashboard/widgets/chart.py:76-80).
  5. Aliases exist but canonical names are safer. Metadata exposes the computation field name and unique class attribute alias, but widget configuration should use the canonical column name.

A correct frameworkapp example:

python
columns = [
    "item_code",              # dimension
    "item_description",       # dimension
    TotalQuantity,            # measure: sum__item_quantity
    TotalValue,               # measure: sum__item_price
]
group_by = "item_code", "item_description"

Step 6 — Enable Filters (Optional)

Widgets can carry parameter values and query-builder filters, but only if the report exposes them.

Parameters

Report parameters come from parameter_spec. A DateRangeParam is special: it produces start_date and end_date, not a single date_range value (enezareporting/parameters.py:139-149). Other parameters expose their name plus any form_fields.

Dashboard widgets may set only allowed parameter names. The complete allowed set is:

  • for a date range: start_date, end_date;
  • for every other parameter: its name and its form_fields.

Query-builder filters

Filters require the report to opt in:

python
class SalesByItem(TabularReportView):
    use_query_builder = True

The dashboard reads get_report_filters_by_model() and exposes the field IDs as meta.filter_fields (enezareporting/dashboard.py:288-307). Widget filter_config rules must use those IDs and operators supported by the workflow engine (enezareporting/dashboard.py:417-466).

If a report does not set use_query_builder = True, any widget filter config on that report fails validation with filters_not_supported.

The filter JSON shape is in Dashboard Reference — Parameters, filters, and periods.


Step 7 — Grant Permissions

Every data source is guarded by an output permission built from the report's app label and slug:

text
output.<app_label>.<report_slug>.view

For example, the frameworkapp report class FrameworktestdocumentSales over Frameworktestdocumentline in frameworkapp has no slug override, so its slug is the lowercased class name and its permission is:

text
output.frameworkapp.frameworktestdocumentsales.view

The slug is derived, not guessed from the class name alone. The reliable way to know the code and permission is to read the metadata after discovery:

python
from enezareporting.dashboard import get_dashboard_report_source

source = get_dashboard_report_source("frameworkapp.frameworktestdocumentsales")
print(source.code)
print(source.meta.output_code)

Then:

  1. Run python manage.py rbac_sync.
  2. Grant the output permission to the roles that should see widgets using this report.
  3. Confirm the report view's own test_func() passes for those users.

list_dashboard_report_sources(user=..., request=...) filters the list by output permission, so a user who can design dashboards may still not see a report they cannot access (enezareporting/dashboard.py:369-384).

Dashboard configuration never grants access to a report or its records. It can only use access the user already has.


Step 8 — Verify the Data Source

Use three checks, in order.

1. It is discovered

python
from enezareporting.dashboard import discover_dashboard_reports

sources = discover_dashboard_reports(force=True)
print(sorted(sources))
print(sources["myapp.salesbyitem"].meta.to_dict())

Expected: the code appears in the dictionary and the metadata contains columns, dimensions, measures, parameters, and an output_code.

2. It can be opened by the designer

Open the report metadata endpoint as a user with the manage action:

text
/dashboards/manage/report-meta/?report=myapp.salesbyitem

Expected: JSON metadata, not {"error": ...} with status 404 (enezadashboard/views/designer.py:599-606).

3. It appears in the create flow

Open /dashboards/manage/create/ and check the Data source dropdown. It lists only eligible reports the current user may execute.

If the report is missing, use Operations and Troubleshooting — The report is missing from the data-source list.


Code and Permission Derivation

The default stable code is:

text
<app_label>.<report_slug>
  • app_label normally comes from the report model's _meta.app_label; the code helper can prefer report_app_label when it is set, so do not set both with different values.
  • report_slug comes from get_report_slug() if defined, otherwise from report_slug, otherwise the lowercased class name.
  • dashboard_code, when set, replaces the full code and is used as-is.

The output permission is built from app_label and slug, not from dashboard_code (enezareporting/dashboard.py:173-185, enezareporting/dashboard.py:317).

Configuration report_ref Output permission
No overrides, class SalesByItem, model in myapp myapp.salesbyitem output.myapp.salesbyitem.view
report_slug = "sales_by_item" myapp.sales_by_item output.myapp.sales_by_item.view
dashboard_code = "sales.v2" sales.v2 output.myapp.salesbyitem.view

Use dashboard_code only when you need a deliberate public code. Otherwise keep the default and let the report's identity stay aligned with its permission.

Do not set report_app_label on a report that has a report_model. The report code helper prefers report_app_label, while discovery metadata prefers the model's app label; setting both differently can make report_ref and the output permission disagree. See Dashboard Reference — Identifiers and Permissions.


Eligibility Rules

Discovery returns only eligible sources. Each condition below makes a report invisible to dashboards (enezareporting/dashboard.py:204-217, enezareporting/dashboard.py:332-355).

Condition Result Internal reason
dashboard_enabled is false or absent Excluded “This report has not opted in to dashboard use.”
Not a supported tabular subtype Excluded “Only tabular reports can be used … in v1.”
report_model cannot be resolved Excluded “The report does not expose a resolvable report_model.”
columns cannot be parsed Excluded “Report columns could not be parsed: …”
Two reports produce the same code Discovery raises ImproperlyConfigured Duplicate dashboard report code.
User lacks the output permission Hidden per user Filtered by list_dashboard_report_sources.

A report with an empty or unreversible report_url_name is still eligible; only its View report link is blank.


What the Dashboard Sees

DashboardReportSource.meta exposes one serializable object. The important fields are:

Field Contents
code, title, description Identity and display text.
app_label, model_label, report_type Where the data comes from.
output_code The RBAC permission code.
report_url_name, report_url Link back to the report page.
columns Name, label, type, format, alignment, aliases, computation name, and supported aggregations.
dimensions Database column names usable as categories and series.
measures Numeric/computation columns and their supported aggregations.
parameters Serialized parameter_spec.
filter_fields, filter_schema Query-builder fields, when use_query_builder is on.
eligible, ineligible_reason Eligibility state. Ineligible sources are not returned by discovery.

The full descriptor contract is in Dashboard Reference — Report metadata contract.


Common Mistakes

Mistake Symptom Fix
Forgot dashboard_enabled = True Report absent everywhere. Add the flag.
Report module never imported Class not in the registry. Import it from views/__init__.py or urls.py.
group_by field missing from columns Field cannot be used as a dimension. Add the field to columns.
Used TotalQuantity instead of sum__item_quantity Field picker / widget validation rejects the value. Use the canonical column name from metadata.
Used a text field as a chart value “Chart value field must be a numeric measure.” Choose a numeric/computation measure.
Two reports create the same computation name Import-time assertion from ComputationField.create. Pass a unique name= or reuse one computation field class.
Duplicate dashboard_code ImproperlyConfigured during discovery. Make codes unique.
report_url_name does not reverse View report link is blank. Register the URL or correct the name.
Changed report code but did not clear the cache Designer still shows old metadata. Call clear_dashboard_report_cache() or restart the process.
User lacks output permission Report missing from that user's data-source list. Run rbac_sync and grant the permission.
dashboard_code changed to a new value Widgets referencing the old code break. Update report_ref values and re-publish.

Completion Checklist

The user should confirm each item manually:

  • [ ] dashboard_enabled = True is set.
  • [ ] The class is imported at startup and appears in discover_dashboard_reports().
  • [ ] report_model resolves and the model is migrated.
  • [ ] Every intended dimension is in columns and group_by.
  • [ ] Every intended measure is a computation field or numeric model field.
  • [ ] The canonical measure names are known (sum__<field> for Sum).
  • [ ] parameter_spec matches the filters widgets should set.
  • [ ] use_query_builder = True if widget filters are required.
  • [ ] report_url_name reverses if the View report link matters.
  • [ ] output.<app_label>.<report_slug>.view exists after rbac_sync.
  • [ ] The report appears in the create-flow data-source dropdown.
  • [ ] The metadata endpoint returns JSON, not an error.