Creating a Dashboard Data Source
Developer guide for the Enezaframework dashboard system. Part of the Dashboards series.
Table of Contents
- Overview
- Prerequisites
- Step 1 — Choose the Report Type
- Step 2 — Declare the Report Class
- Step 3 — Opt In to Dashboards
- Step 4 — Make Sure the Class Is Registered
- Step 5 — Expose Dimensions and Measures
- Step 6 — Enable Filters (Optional)
- Step 7 — Grant Permissions
- Step 8 — Verify the Data Source
- Code and Permission Derivation
- Eligibility Rules
- What the Dashboard Sees
- Common Mistakes
- Completion Checklist
- 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:
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.pyre-exported bymyapp/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:
class SalesReport(TabularReportView):
...
Set design_slug on the class when you need the report designer's template behavior;
no DesignTemplateMixin is needed:
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.
# 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:
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:
myapp/views/__init__.pyimports the report module (frameworkapp does this atframeworkapp/views/__init__.py:1).myapp/urls.pyimports the class to build apath(...).
If neither happens, the class never registers and no dashboard can see it.
# 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:
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:
- A
group_byfield must also be a report column to appear as a dimension. Ifgroup_by = "item_code"but"item_code"is not incolumns, the designer cannot offer it. - Computation fields become measures automatically. Their canonical name is
<method>__<field>, for examplesum__item_quantity(enezareporting/fields.py:76-99). Use that canonical name in widget configuration. - Numeric model fields can also be used as measures and support
sum,avg,min,max, andcount(enezareporting/dashboard.py:147-156). - Non-numeric fields cannot be measures. A chart value field must be one of
meta.measures(enezadashboard/widgets/chart.py:76-80). - 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:
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
nameand itsform_fields.
Query-builder filters
Filters require the report to opt in:
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:
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:
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:
from enezareporting.dashboard import get_dashboard_report_source
source = get_dashboard_report_source("frameworkapp.frameworktestdocumentsales")
print(source.code)
print(source.meta.output_code)
Then:
- Run
python manage.py rbac_sync. - Grant the output permission to the roles that should see widgets using this report.
- 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
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:
/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:
<app_label>.<report_slug>
app_labelnormally comes from the report model's_meta.app_label; the code helper can preferreport_app_labelwhen it is set, so do not set both with different values.report_slugcomes fromget_report_slug()if defined, otherwise fromreport_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 = Trueis set. - [ ] The class is imported at startup and appears in
discover_dashboard_reports(). - [ ]
report_modelresolves and the model is migrated. - [ ] Every intended dimension is in
columnsandgroup_by. - [ ] Every intended measure is a computation field or numeric model field.
- [ ] The canonical measure names are known (
sum__<field>for Sum). - [ ]
parameter_specmatches the filters widgets should set. - [ ]
use_query_builder = Trueif widget filters are required. - [ ]
report_url_namereverses if the View report link matters. - [ ]
output.<app_label>.<report_slug>.viewexists afterrbac_sync. - [ ] The report appears in the create-flow data-source dropdown.
- [ ] The metadata endpoint returns JSON, not an error.