Worked Example: frameworkapp
A complete data-source and dashboard walkthrough using the real example that ships with the repository. Part of the Dashboards series.
Table of Contents
- What You Will Trace
- The Files Involved
- Step 1 — Understand the Data Source
- Step 2 — Verify the Metadata
- Step 3 — Read the Definition
- Step 4 — Seed the Dashboard
- Step 5 — Design It in the Browser
- Step 6 — Verify the Result
- Extension Exercises
- Related Guides
What You Will Trace
The repository contains a working pair:
- Data source:
FrameworktestdocumentSales, a tabular report overFrameworktestdocumentline. - Dashboard:
procurement_overview, seeded fromframeworkapp/dashboards.py.
By the end, you will know exactly why each line exists and how to repeat the pattern in another app.
This is an existing example, not a test fixture you must run. Per project rules, the agent does not execute the steps. The user is solely responsible for testing and validating the result.
The Files Involved
| File | What it contains |
|---|---|
frameworkapp/models.py:90-102 |
Frameworktestdocumentline model. |
frameworkapp/views/report.py:13-55 |
FrameworktestdocumentSales data source. |
frameworkapp/views/report.py:58-84 |
A second, similar report. |
frameworkapp/urls.py:25-26 |
Report URLs used by report_url_name. |
frameworkapp/dashboards.py:1-117 |
DASHBOARDS definition. |
frameworkapp/apps.py:1-16 |
dashboards = DASHBOARDS on the app config. |
frameworkapp/views/__init__.py:1-3 |
Imports the report module so the class registers. |
Step 1 — Understand the Data Source
The model
Frameworktestdocumentline has the fields the dashboard will use:
class Frameworktestdocumentline(EnezaDocumentLines):
frameworktestdocument = models.ForeignKey(
Frameworktestdocument,
on_delete=models.CASCADE,
verbose_name="Document",
related_name="frameworktestdocument_lines",
)
item_code = models.CharField(max_length=100, verbose_name="Item Code")
item_description = models.CharField(max_length=200, verbose_name="Item Description")
item_quantity = models.IntegerField(verbose_name="Item Quantity")
item_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="Item Price")
...
item_code and item_description are text dimensions. item_quantity and
item_price are numeric and can become measures.
The report class
class FrameworktestdocumentSales(TabularReportView):
design_slug = "frameworkappframeworktestdocumentline"
report_name = "Framework Test Document Sales"
dashboard_enabled = True
dashboard_entity_scoped = True
report_url_name = "frameworkapp:frameworktestreport"
report_model = Frameworktestdocumentline
date_field = "created_at"
report_generator_class = ListViewReportGenerator
use_query_builder = True
use_modal_filter = True
SumItemQuantity = ComputationField.create(
Sum, "item_quantity", verbose_name="Total Quantity"
)
SumItemPrice = ComputationField.create(
Sum, "item_price", verbose_name="Total Price"
)
columns = [
"item_code",
"item_description",
SumItemQuantity,
SumItemPrice,
]
group_by = "item_code", "item_description"
parameter_spec = ParameterSet(
DateRangeParam(name="date_range", start_field="created_at"),
)
Why each part matters:
| Declaration | Effect |
|---|---|
TabularReportView |
Makes the class a supported single-parent report with a form, generator, page, designer rendering, and totals hooks. |
dashboard_enabled = True |
Opts it in as a data source. |
dashboard_entity_scoped = True |
Dashboard execution calls for_entity(request=request) when available. |
report_url_name = "frameworkapp:frameworktestreport" |
Supplies the View report link. |
report_model |
Provides the app label (frameworkapp) and the model for date fields and aggregates. |
date_field = "created_at" |
Applies dashboard periods when no better date field is chosen. |
use_query_builder = True |
Enables dashboard filters. |
SumItemQuantity / SumItemPrice |
Become measures named sum__item_quantity / sum__item_price. |
columns |
Exposes two dimensions and two measures. |
group_by |
Groups the report rows by item. Both fields are also columns, so both are selectable. |
DateRangeParam(start_field="created_at") |
Produces start_date and end_date form fields. |
The derived identity
app_label = frameworkapp (from Frameworktestdocumentline)
report_slug = frameworktestdocumentsales (lowercased class name)
report_ref = frameworkapp.frameworktestdocumentsales
output_code = output.frameworkapp.frameworktestdocumentsales.view
Widgets in dashboards.py reference exactly
frameworkapp.frameworktestdocumentsales.
The second report,
FrameworktestdocumentlineSchemaReport, produces a different code:frameworkapp.frameworktestdocumentlineschemareport.
Step 2 — Verify the Metadata
The designer uses metadata to populate field pickers. As a user with the manage action, open:
/dashboards/manage/report-meta/?report=frameworkapp.frameworktestdocumentsales
Expected shape (abridged):
{
"code": "frameworkapp.frameworktestdocumentsales",
"title": "Framework Test Document Sales",
"app_label": "frameworkapp",
"report_type": "tabular",
"model_label": "frameworkapp.frameworktestdocumentline",
"output_code": "output.frameworkapp.frameworktestdocumentsales.view",
"dimensions": ["item_code", "item_description"],
"measures": [
{
"name": "sum__item_quantity",
"label": "Total Quantity",
"aggregations": ["sum"],
"aliases": ["sum__item_quantity", "SumItemQuantity"]
},
{
"name": "sum__item_price",
"label": "Total Price",
"aggregations": ["sum"],
"aliases": ["sum__item_price", "SumItemPrice"]
}
],
"eligible": true
}
Exact aliases, types, and filter fields come from the parser; the important
parts are the two dimensions, the two measures, and eligible: true.
If this endpoint returns 404, use Operations and Troubleshooting.
Step 3 — Read the Definition
The definition is intentionally ordinary Python:
DASHBOARDS = [
{
"code": "procurement_overview",
"module": "frameworkapp",
"name": "Procurement overview",
"description": "Sales volume, product mix, and item-level detail from the framework test document lines.",
"is_default": True,
"sort_order": 0,
"managed_by": "seed",
"layout_template": "",
"default_report_ref": "frameworkapp.frameworktestdocumentsales",
"publish": True,
"tabs": [...],
}
]
| Key | Meaning in this example |
|---|---|
module |
frameworkapp owns the dashboard. |
code |
procurement_overview; stable with the module. |
is_default |
Users without a preference land here for the module. |
managed_by: seed |
Re-running the seeder updates it. |
default_report_ref |
Pre-binds the wireframe-created slots if this dashboard is ever recreated from the create flow. Seeded widgets still declare their own references. |
publish: True |
Publishes the seeded draft. |
Tab 1 — Overview
{
"code": "overview",
"name": "Overview",
"icon": "squares-2x2",
"sort_order": 0,
"filter_config": {"date_range": {"enabled": True}},
"widgets": [
{
"code": "total_quantity",
"title": "Total quantity",
"type": "kpi",
"report_ref": "frameworkapp.frameworktestdocumentsales",
"display_config": {
"measure": "sum__item_quantity",
"aggregation": "sum",
"format": "number",
},
"layout": {"x": 0, "y": 0, "w": 3, "h": 2},
},
...
],
}
filter_config.date_range.enabledenables the tab's date defaults. No explicitdefault_periodis set, so the dashboard/site default applies — by default, the last 30 days.total_quantityuses the Sum computation field as a true aggregate.total_valueuses the same pattern withformat: "currency"anddecimals: 2.quantity_by_itemis a bar chart:category_field: "item_code",value_field: "sum__item_quantity",limit: 10,sort: "desc".sales_detailis a table over all four columns withpage_size: 10.
Tab 2 — Breakdown
value_shareis a donut chart overitem_codebysum__item_price, limited to 8 slices.previewis a non-paginated preview of three columns withrow_limit: 5.
Layout map
Overview tab (12-column grid)
0 3 6 12
+---------+---------+-------------------+
| KPI | KPI | | y=0, h=2
| qty 3x2 | value | |
+---------+---------+-------------------+
| | chart | chart: x=6, y=0, w=6, h=4
| | quantity by item |
+-------------------+-------------------+
| table: item detail, x=0, y=4, w=12, h=5
+-----------------------------------------+
The positions satisfy x + w <= 12, w 2–12, and h 2–8.
Step 4 — Seed the Dashboard
From the project root:
python manage.py seed_dashboards --dry-run --json
python manage.py seed_dashboards
python manage.py rbac_sync
Expected dry-run behavior:
- validates both tabs and all six widgets;
- reports that
frameworkapp.procurement_overviewwould be seeded (or skipped if it is currently administrator-owned); - exits non-zero if any definition is invalid.
The real run reports counts, for example:
Dashboards: 1 created, 0 updated, 0 skipped, 1 published, 0 pruned.
Counts depend on database state. A new dashboard is published by default because
the definition sets "publish": True.
rbac_sync discovers the report output permissions and the dashboard manage
action. Without it, designers and viewers may not see the dashboard or its
widgets.
If the dashboard already exists as admin
Seeding skips administrator-owned dashboards unless forced:
python manage.py seed_dashboards --dashboard procurement_overview --force
Use --force deliberately: it overwrites administrator edits with the
definition.
Step 5 — Design It in the Browser
- Open
/dashboards/manage/. - Open Procurement overview.
- Select the Overview tab.
- Open
total_quantity. - Data shows the report andkpitype. - Display showssum__item_quantityandsum. - Layout showsx=0, y=0, w=3, h=2. - Change the aggregation to
avgand save. The draft changes; the live dashboard does not. - Preview the tab. The KPI now shows the average across grouped report rows.
- Revert to
sumand publish, or take ownership first if you intend to keep browser edits permanently.
Seed ownership warning
procurement_overview is managed_by: seed. If you edit it in the browser and
later re-run seed_dashboards, your edits are overwritten. Either:
- click Take ownership to switch it to
admin, or - make the change in
frameworkapp/dashboards.pyand re-seed.
Use the designer for exploration; use code for changes that must survive deployment.
Step 6 — Verify the Result
The user should confirm each item manually:
- [ ]
/dashboards/frameworkapp/redirects to/dashboards/frameworkapp/procurement_overview/(because it is the default). - [ ] The dashboard shows two tabs: Overview and Breakdown.
- [ ]
Total quantityshows a number, not an error. - [ ]
Total valueshows a currency-formatted number. - [ ]
Quantity by itemrenders a bar chart with up to 10 categories. - [ ]
Item detailrenders a paginated, sortable table. - [ ] The Breakdown tab shows a donut and a five-row preview.
- [ ] Changing the period changes the KPI and chart values.
- [ ] The View report links open the frameworkapp report page.
- [ ] A user without
output.frameworkapp.frameworktestdocumentsales.viewsees a permission error or no data, not the underlying rows. - [ ] The published page does not change when the draft is edited.
- [ ] Publishing a draft validates and creates a new draft.
If data appears empty, check the period, the entity scope, and whether test documents exist. The dashboard does not create data.
Extension Exercises
Add a count KPI
{
"code": "line_count",
"title": "Line count",
"type": "kpi",
"report_ref": "frameworkapp.frameworktestdocumentsales",
"display_config": {
"aggregation": "count",
"format": "integer",
},
"layout": {"x": 9, "y": 9, "w": 3, "h": 2},
}
count is the only aggregation allowed without a measure
(enezadashboard/widgets/kpi.py:23-29).
Add a donut chart to the Overview tab
{
"code": "value_mix",
"title": "Value mix",
"type": "chart",
"report_ref": "frameworkapp.frameworktestdocumentsales",
"display_config": {
"chart_type": "donut",
"category_field": "item_code",
"value_field": "sum__item_price",
"limit": 8,
},
"layout": {"x": 0, "y": 9, "w": 6, "h": 4},
}
Remember that donut charts reject negative values at runtime.
Add a third tab
{
"code": "items",
"name": "Items",
"icon": "cube",
"sort_order": 2,
"widgets": [
{
"code": "item_table",
"title": "Items",
"type": "table",
"report_ref": "frameworkapp.frameworktestdocumentsales",
"display_config": {
"columns": ["item_code", "item_description", "sum__item_price"],
"page_size": 20,
},
"layout": {"x": 0, "y": 0, "w": 12, "h": 6},
}
],
}
Then:
python manage.py seed_dashboards --dry-run
python manage.py seed_dashboards
Because the dashboard is seed-owned until someone takes ownership, the new tab
is added to the draft and published if publish is true or --publish is
passed.
Change the module default
Set is_default: False on procurement_overview and add another published
dashboard with is_default: True. Only one active default per module is allowed.